diff --git a/Assets/Scripts/Models/Scene/ManipulatorState.cs b/Assets/Scripts/Models/Scene/ManipulatorState.cs index f4a076e7..5bb4ac4e 100644 --- a/Assets/Scripts/Models/Scene/ManipulatorState.cs +++ b/Assets/Scripts/Models/Scene/ManipulatorState.cs @@ -67,7 +67,7 @@ public record ManipulatorState /// /// Drive past target distance (µm). /// - public int DrivePastDistance; + public int DrivePastDistance = 50; #endregion diff --git a/Assets/Scripts/Models/Scene/SceneReducers.cs b/Assets/Scripts/Models/Scene/SceneReducers.cs index 54868ec5..b41378b4 100644 --- a/Assets/Scripts/Models/Scene/SceneReducers.cs +++ b/Assets/Scripts/Models/Scene/SceneReducers.cs @@ -750,13 +750,16 @@ public static SceneState SetTargetInsertionProbeNameReducer( try { - // Verify selected target exists and is targetable. + // Verify selected target exists and is targetable: + // 1. Target name must not be empty + // 2. Target probe must exist in the probes list + // 3. Target probe must NOT be a visualization probe (those are manipulator-controlled) if ( string.IsNullOrEmpty(action.payload.targetName) || !state.Probes.Exists(probeState => probeState.Name == action.payload.targetName ) - || !state.Manipulators.Exists(manipulatorState => + || state.Manipulators.Exists(manipulatorState => manipulatorState.VisualizationProbeName == action.payload.targetName ) ) diff --git a/Assets/Scripts/Pinpoint/Probes/ProbeController.cs b/Assets/Scripts/Pinpoint/Probes/ProbeController.cs index bf6745dc..e5ddacb0 100644 --- a/Assets/Scripts/Pinpoint/Probes/ProbeController.cs +++ b/Assets/Scripts/Pinpoint/Probes/ProbeController.cs @@ -36,11 +36,6 @@ public void Register(ProbeManager probeManager) /// public Vector3 VisualizationLocalAPMLDV { get; set; } - /// - /// Local field for visualization probe depth. - /// - public float VisualizationLocalDepth { get; set; } - /// /// Local field for visualization probe angles. /// diff --git a/Assets/Scripts/Services/EphysLinkService.cs b/Assets/Scripts/Services/EphysLinkService.cs index 9967080d..089f0b5d 100644 --- a/Assets/Scripts/Services/EphysLinkService.cs +++ b/Assets/Scripts/Services/EphysLinkService.cs @@ -704,16 +704,6 @@ var manipulatorState in sceneState.Manipulators.Where(state => referenceCoordinateAdjustedWorldPosition ); - // Cancel update if the manipulator's position did not change by a lot. - var probeState = sceneState.Probes.FirstOrDefault(state => - state.Name == manipulatorState.VisualizationProbeName - ); - if ( - probeState == null - || Vector3.SqrMagnitude(transformedAPMLDV - probeState.APMLDV) < 0.0001f - ) - continue; - // Get the current forward vector of the probe. var forwardT = BrainAtlasManager.ActiveAtlasTransform.U2T_Vector( BrainAtlasManager.ActiveReferenceAtlas.World2Atlas_Vector( @@ -726,18 +716,20 @@ var manipulatorState in sceneState.Manipulators.Where(state => if (probeController == null) continue; - var depth = sceneState.NumberOfAxesOnManipulator switch + var depthToApply = sceneState.NumberOfAxesOnManipulator switch { - 3 => duraOffsetAdjustment, + 3 => duraOffsetAdjustment, // Positive moves probe forward/deeper into brain 4 => referenceCoordinateAdjustedManipulatorPosition.w, _ => throw new ValueOutOfRangeException( "Number of axes on manipulator is invalid." ), }; + // Bake depth into APMLDV along forward vector + var finalAPMLDV = transformedAPMLDV + forwardT * depthToApply; + // Write position data directly to ProbeController's local fields - probeController.VisualizationLocalAPMLDV = transformedAPMLDV; - probeController.VisualizationLocalDepth = depth; + probeController.VisualizationLocalAPMLDV = finalAPMLDV; probeController.VisualizationLocalAngles = manipulatorState.Angles; probeController.VisualizationLocalForwardT = forwardT; diff --git a/Assets/Scripts/UI/ViewModels/AutomationInspectorViewModel.cs b/Assets/Scripts/UI/ViewModels/AutomationInspectorViewModel.cs index 7f5cbf09..c88f2428 100644 --- a/Assets/Scripts/UI/ViewModels/AutomationInspectorViewModel.cs +++ b/Assets/Scripts/UI/ViewModels/AutomationInspectorViewModel.cs @@ -1,9 +1,16 @@ +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; +using System.Threading; using BrainAtlas; +using BrainAtlas.CoordinateSystems; +using EphysLink; +using KS.Diagnostics; using Models; using Models.Scene; +using Pinpoint.CoordinateSystems; using Services; using Unity.AppUI.MVVM; using Unity.AppUI.Redux; @@ -24,6 +31,16 @@ public partial class AutomationInspectorViewModel private string ActiveManipulatorId => _storeService.Store.GetState(SliceNames.SCENE_SLICE).ActiveManipulatorId; + /// + /// Helper to get a probe's world state from Redux. + /// + private ProbeWorldState GetProbeWorldState(string probeName) + { + return _storeService.Store + .GetState(SliceNames.PROBE_WORLD_SLICE) + .GetProbeWorldState(probeName); + } + #endregion #region Properties @@ -85,6 +102,79 @@ public partial class AutomationInspectorViewModel #endregion + #region Trajectory and Visualization Fields + + /// + /// Trajectory broken into 3 stages (for 3 axes of movement). + /// + /// Execution order: DV, AP, ML. Defaults to negative infinity when there is no trajectory. + private (Vector3 first, Vector3 second, Vector3 third) _trajectoryCoordinates = ( + Vector3.negativeInfinity, + Vector3.negativeInfinity, + Vector3.negativeInfinity + ); + + /// + /// Record the depth at the entry coordinate. + /// + /// Used during insertion to calculate the actual distance needed to retract back to the entry coordinate. + private float _entryCoordinateDepth; + + /// + /// Trajectory line GameObjects. + /// + private (GameObject ap, GameObject ml, GameObject dv) _trajectoryLineGameObjects; + + /// + /// Trajectory line renderers. + /// + private (LineRenderer ap, LineRenderer ml, LineRenderer dv) _trajectoryLineRenderers; + + // Axes colors + private static readonly Color AP_COLOR = new(1, 0.3215686f, 0.3215686f); // Red + private static readonly Color ML_COLOR = new(0.2039216f, 0.6745098f, 0.8784314f); // Blue + private static readonly Color DV_COLOR = new(1, 0.854902f, 0.4745098f); // Yellow + + // Trajectory line properties + private const float LINE_WIDTH = 0.1f; + private const int NUM_SEGMENTS = 2; + + // Safety margin + private const float IDEAL_ENTRY_COORDINATE_TO_DURA_DISTANCE = 3.5f; + + // Movement speed + private const float AUTOMATIC_MOVEMENT_SPEED = 0.5f; // mm/s + #endregion + + #region Insertion Constants + + /// + /// Distance from target to start slowing down probe (mm). + /// + private const float NEAR_TARGET_DISTANCE = 1f; + + /// + /// Slowdown factor for the probe when it is near the target. + /// + private const float NEAR_TARGET_SPEED_MULTIPLIER = 2f / 3f; + + /// + /// Extra speed multiplier for the probe when it is exiting. + /// + private const int EXIT_DRIVE_SPEED_MULTIPLIER = 6; + + /// + /// Extra safety margin for the Dura to outside (mm). + /// + private const float DURA_MARGIN_DISTANCE = 0.2f; + + /// + /// Cancellation token source for stopping ongoing insertion/exit operations. + /// + private CancellationTokenSource _insertionDriveCts; + + #endregion + public AutomationInspectorViewModel( StoreService storeService, EphysLinkService ephysLinkService @@ -133,11 +223,8 @@ private void OnSceneStateChanged(SceneState state) // 3. Is in the brain. .Where(probeState => { - var probeManager = ProbeManager.Instances.FirstOrDefault(manager => - manager.name == probeState.Name - ); - return probeManager != null - && probeManager.CalculateEntryCoordinate().probeInBrain; + var probeWorldState = GetProbeWorldState(probeState.Name); + return probeWorldState != null && probeWorldState.IsProbeInBrain; }) // 4. Is not already selected by other manipulator probes (unless it was selected by this active probe). .Where(probeState => @@ -161,10 +248,13 @@ private void OnSceneStateChanged(SceneState state) var selectedTargetInsertionProbeState = state.Probes.FirstOrDefault(probeState => probeState.Name == state.ActiveManipulatorState.TargetInsertionProbeName ); - TargetInsertionProbeIndex = (selectedTargetInsertionProbeState == null - || !TargetInsertionProbeStates.Contains(selectedTargetInsertionProbeState)) - ? -1 - : TargetInsertionProbeStates.IndexOf(selectedTargetInsertionProbeState); + TargetInsertionProbeIndex = + ( + selectedTargetInsertionProbeState == null + || !TargetInsertionProbeStates.Contains(selectedTargetInsertionProbeState) + ) + ? -1 + : TargetInsertionProbeStates.IndexOf(selectedTargetInsertionProbeState); // Update dura offset. DuraOffset = state.ActiveManipulatorState.DuraOffset; @@ -202,6 +292,9 @@ private void OnShuttingDown() { App.shuttingDown -= OnShuttingDown; _sceneStateSubscription.Dispose(); + + // Clean up trajectory lines + RemoveTrajectoryLines(); } #region Commands @@ -232,17 +325,33 @@ private void SelectTargetInsertionProbe(int index) ResetTargetInsertionProbeSelection(); return; } - - // Set the selected target insertion probe name in the store. - var selectedTargetInsertionProbeState = TargetInsertionProbeStates.ElementAt( - index - ); + + // Get the selected target insertion probe + var selectedTargetInsertionProbeState = TargetInsertionProbeStates.ElementAt(index); + + // Validate the target probe world state exists + var targetProbeWorldState = GetProbeWorldState(selectedTargetInsertionProbeState.Name); + if (targetProbeWorldState == null) + { + Debug.LogError( + $"Target probe world state not found: {selectedTargetInsertionProbeState.Name}" + ); + return; + } + + // Set the selected target insertion probe name in the store _storeService.Store.Dispatch( SceneActions.SET_TARGET_INSERTION_PROBE_NAME, (ActiveManipulatorId, selectedTargetInsertionProbeState.Name) ); - // TODO: Call ComputeEntryCoordinateTrajectory once it has been converted. + // Compute and visualize trajectory with the target probe name + var entryCoordinate = ComputeTargetEntryCoordinateTrajectory(selectedTargetInsertionProbeState.Name); + + if (float.IsNegativeInfinity(entryCoordinate.x)) + { + Debug.LogWarning("Failed to compute trajectory for selected target probe"); + } } [ICommand] @@ -253,56 +362,216 @@ private void ResetTargetInsertionProbeSelection() SceneActions.SET_TARGET_INSERTION_PROBE_NAME, (ActiveManipulatorId, string.Empty) ); + + // Remove trajectory visualization + RemoveTrajectoryLines(); } [ICommand] - private void DriveToTargetEntryCoordinate() + private async void DriveToTargetEntryCoordinate() { - ProbeService - .DriveActiveProbeToTargetEntryCoordinate() - .ContinueWith(task => + // Capture manipulator ID and state at command start for async safety. + var manipulatorId = ActiveManipulatorId; + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var manipulatorState = sceneState.ActiveManipulatorState; + + // Validate trajectory exists + if (float.IsNegativeInfinity(_trajectoryCoordinates.first.x)) + { + Debug.LogError($"No trajectory planned for manipulator {manipulatorId}"); + return; + } + + // Convert coordinates to manipulator positions + var dvPosition = ConvertInsertionAPMLDVToManipulatorPosition( + _trajectoryCoordinates.first, + manipulatorState + ); + var apPosition = ConvertInsertionAPMLDVToManipulatorPosition( + _trajectoryCoordinates.second, + manipulatorState + ); + var mlPosition = ConvertInsertionAPMLDVToManipulatorPosition( + _trajectoryCoordinates.third, + manipulatorState + ); + + // Check if conversion failed + if (dvPosition == null || apPosition == null || mlPosition == null) + { + HandleDriveFailed( + "Failed to convert trajectory coordinates to manipulator positions", + manipulatorId + ); + return; + } + + // Set state to driving + _storeService.Store.Dispatch( + SceneActions.SET_AUTOMATION_PROGRESS_STATE, + (manipulatorId, AutomationProgressState.DrivingToTargetEntryCoordinate) + ); + + // Log that movement is starting + OutputLog.Log( + new[] { - // Do not proceed if the drive failed. - if (!task.Result) - return; + "Automation", + DateTime.Now.ToString(CultureInfo.InvariantCulture), + "DriveToTargetEntryCoordinate", + manipulatorId, + "Start", + } + ); - // Complete the drive state if successful. - _storeService.Store.Dispatch( - SceneActions.COMPLETE_AUTOMATION_INTERMEDIATE_PROGRESS, - ActiveManipulatorId - ); - }); + // Stage 1: DV movement + var dvResponse = await _ephysLinkService.SetPosition( + new SetPositionRequest( + manipulatorId, + dvPosition.Value, + AUTOMATIC_MOVEMENT_SPEED + ) + ); + + if (EphysLinkService.HasError(dvResponse.Error)) + { + HandleDriveFailed("Failed to move to DV position", manipulatorId); + return; + } + + // Stage 2: AP movement + var apResponse = await _ephysLinkService.SetPosition( + new SetPositionRequest( + manipulatorId, + apPosition.Value, + AUTOMATIC_MOVEMENT_SPEED + ) + ); + + if (EphysLinkService.HasError(apResponse.Error)) + { + HandleDriveFailed("Failed to move to AP position", manipulatorId); + return; + } + + // Stage 3: ML movement + var mlResponse = await _ephysLinkService.SetPosition( + new SetPositionRequest( + manipulatorId, + mlPosition.Value, + AUTOMATIC_MOVEMENT_SPEED + ) + ); + + if (EphysLinkService.HasError(mlResponse.Error)) + { + HandleDriveFailed("Failed to move to ML position", manipulatorId); + return; + } + + // Record entry coordinate depth + var finalPositionResponse = await _ephysLinkService.GetPosition(manipulatorId); + if (EphysLinkService.HasError(finalPositionResponse.Error)) + { + HandleDriveFailed("Failed to get final position at entry coordinate", manipulatorId); + return; + } + + _entryCoordinateDepth = finalPositionResponse.Position.w; + + // Remove visualization lines + RemoveTrajectoryLines(); + + // Complete the drive state + _storeService.Store.Dispatch( + SceneActions.COMPLETE_AUTOMATION_INTERMEDIATE_PROGRESS, + manipulatorId + ); + + // Log completion + OutputLog.Log( + new[] + { + "Automation", + DateTime.Now.ToString(CultureInfo.InvariantCulture), + "DriveToTargetEntryCoordinate", + manipulatorId, + "Finish", + } + ); + } + + private void HandleDriveFailed(string errorMessage, string manipulatorId) + { + Debug.LogError(errorMessage); + OutputLog.Log( + new[] + { + "Automation", + DateTime.Now.ToString(CultureInfo.InvariantCulture), + "DriveToTargetEntryCoordinate", + manipulatorId, + $"Failed: {errorMessage}", + } + ); + + // Revert state to calibrated + _storeService.Store.Dispatch( + SceneActions.CANCEL_AUTOMATION_INTERMEDIATE_PROGRESS, + manipulatorId + ); } [ICommand] - private void StopDriveToTargetEntryCoordinate() + private async void StopDriveToTargetEntryCoordinate() { - ProbeService - .StopActiveProbeDriveToTargetEntryCoordinate() - .ContinueWith(task => + // Log stop request + OutputLog.Log( + new[] { - // Do not proceed if the drive failed. - if (!task.Result) - return; + "Automation", + DateTime.Now.ToString(CultureInfo.InvariantCulture), + "DriveToTargetEntryCoordinate", + ActiveManipulatorId, + "Stopped", + } + ); - // Reset back to calibrated state. - _storeService.Store.Dispatch( - SceneActions.SET_AUTOMATION_PROGRESS_STATE, - (ActiveManipulatorId, AutomationProgressState.IsCalibrated) - ); - }); + // Send stop command to manipulator + var stopResponse = await _ephysLinkService.Stop(ActiveManipulatorId); + + // Check for errors + if (EphysLinkService.HasError(stopResponse)) + { + Debug.LogError($"Failed to stop drive: {stopResponse}"); + return; + } + + // Cancel intermediate progress (revert to IsCalibrated state) + _storeService.Store.Dispatch( + SceneActions.CANCEL_AUTOMATION_INTERMEDIATE_PROGRESS, + ActiveManipulatorId + ); + + // Note: Do NOT remove trajectory lines on stop - user may want to restart } [ICommand] private void ResetDuraOffset() { _storeService.Store.Dispatch(SceneActions.RESET_DURA_OFFSET, ActiveManipulatorId); + + // If offset is reset, we have no idea where we could be so we reset to IsCalibrated. + _storeService.Store.Dispatch(SceneActions.SET_AUTOMATION_PROGRESS_STATE, (ActiveManipulatorId, AutomationProgressState.IsCalibrated)); } [ICommand] private async void RecalculateDuraOffset() { await _ephysLinkService.SetManipulatorDuraOffsetToCurrentDepth(ActiveManipulatorId); + + // After recalculating the dura offset, we are at the Dura insert state. + _storeService.Store.Dispatch(SceneActions.SET_AUTOMATION_PROGRESS_STATE, (ActiveManipulatorId, AutomationProgressState.AtDuraInsert)); } [ICommand] @@ -338,37 +607,934 @@ private void SetCustomInsertionSpeed(int customSpeed) } [ICommand] - private void InsertionDrive() + private async void InsertionDrive() { - // State is updated externally by the ProbeService. - // _probeService.InsertionDriveActiveProbe(); + // Validate state is insertable. + if (!IsInsertable(AutomationProgressState)) + { + Debug.LogError( + $"Cannot drive: not in insertable state. Current state: {AutomationProgressState}" + ); + return; + } + + // Capture manipulator ID and state at command start for async safety. + var manipulatorId = ActiveManipulatorId; + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var manipulatorState = sceneState.ActiveManipulatorState; + var targetProbeName = manipulatorState.TargetInsertionProbeName; + + var targetProbeWorldState = GetProbeWorldState(targetProbeName); + if (targetProbeWorldState == null) + { + Debug.LogError( + $"Target probe world state not found: {targetProbeName}" + ); + return; + } + + // Convert speed from µm/s to mm/s. + var baseSpeed = CustomInsertionSpeed / 1000f; + var drivePastDistance = DrivePastDistance / 1000f; + + // Set up cancellation token. + _insertionDriveCts = new CancellationTokenSource(); + + // Store initial ETA for progress calculation. + _originalETA = ComputeEtaSeconds(targetProbeName, baseSpeed, drivePastDistance, manipulatorState); + + try + { + while (AutomationProgressState != AutomationProgressState.AtTarget) + { + // Check for cancellation. + _insertionDriveCts.Token.ThrowIfCancellationRequested(); + + // Get target depth. + var targetDepth = GetTargetDepth(targetProbeName, manipulatorState); + + // Set state to next driving state. + _storeService.Store.Dispatch( + SceneActions.SET_AUTOMATION_PROGRESS_STATE_TO_NEXT_DRIVING, + manipulatorId + ); + + // Log set to driving state. + LogDriveToTargetInsertion(manipulatorId, targetDepth, baseSpeed, drivePastDistance); + + // Update ETA. + Eta = ComputeEtaSeconds(targetProbeName, baseSpeed, drivePastDistance, manipulatorState); + DriveProgressPercentage = + _originalETA > 0 ? 1f - (float)Eta / _originalETA : 0f; + + // Handle driving state. + switch (AutomationProgressState) + { + case AutomationProgressState.DrivingToNearTarget: + // Drive to near target if not already there. + if ( + GetCurrentDistanceToTarget(targetProbeName, manipulatorState) + > NEAR_TARGET_DISTANCE + ) + { + var driveToNearTargetResponse = await _ephysLinkService.SetDepth( + new SetDepthRequest( + manipulatorId, + targetDepth - NEAR_TARGET_DISTANCE, + baseSpeed + ) + ); + + if (EphysLinkService.HasError(driveToNearTargetResponse.Error)) + { + Debug.LogError( + $"Failed to drive to near target: {driveToNearTargetResponse.Error}" + ); + return; + } + } + break; + + case AutomationProgressState.DrivingToPastTarget: + // Drive past target at reduced speed. + var driveToPastTargetResponse = await _ephysLinkService.SetDepth( + new SetDepthRequest( + manipulatorId, + targetDepth + drivePastDistance, + baseSpeed * NEAR_TARGET_SPEED_MULTIPLIER + ) + ); + + if (EphysLinkService.HasError(driveToPastTargetResponse.Error)) + { + Debug.LogError( + $"Failed to drive past target: {driveToPastTargetResponse.Error}" + ); + return; + } + break; + + case AutomationProgressState.ReturningToTarget: + // Return to target at reduced speed. + var returnToTargetResponse = await _ephysLinkService.SetDepth( + new SetDepthRequest( + manipulatorId, + targetDepth, + baseSpeed * NEAR_TARGET_SPEED_MULTIPLIER + ) + ); + + if (EphysLinkService.HasError(returnToTargetResponse.Error)) + { + Debug.LogError( + $"Failed to return to target: {returnToTargetResponse.Error}" + ); + return; + } + break; + } + + // Complete this phase. + _storeService.Store.Dispatch( + SceneActions.COMPLETE_AUTOMATION_INTERMEDIATE_PROGRESS, + manipulatorId + ); + + // Log the event. + LogDriveToTargetInsertion(manipulatorId, targetDepth, baseSpeed, drivePastDistance); + } + + // Drive complete - reset ETA and progress. + Eta = 0; + DriveProgressPercentage = 1f; + } + catch (OperationCanceledException) + { + // Stopped by user - state already handled by StopInsertionDrive. + } + finally + { + _insertionDriveCts?.Dispose(); + _insertionDriveCts = null; + } } [ICommand] - private void InsertionExit() + private async void InsertionExit() { - // State is updated externally by the ProbeService. - // _ = _probeService.InsertionExitActiveProbe(); + // Validate state is exitable. + if (!IsExitable(AutomationProgressState)) + { + Debug.LogError( + $"Cannot exit: not in exitable state. Current state: {AutomationProgressState}" + ); + return; + } + + // Capture manipulator ID and state at command start for async safety. + var manipulatorId = ActiveManipulatorId; + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var manipulatorState = sceneState.ActiveManipulatorState; + var targetProbeName = manipulatorState.TargetInsertionProbeName; + + var targetProbeWorldState = GetProbeWorldState(targetProbeName); + if (targetProbeWorldState == null) + { + Debug.LogError( + $"Target probe world state not found: {targetProbeName}" + ); + return; + } + + // Convert speed from µm/s to mm/s. + var baseSpeed = CustomInsertionSpeed / 1000f; + var drivePastDistance = DrivePastDistance / 1000f; + + // Set up cancellation token. + _insertionDriveCts = new CancellationTokenSource(); + + // Store initial ETA for progress calculation. + _originalETA = ComputeEtaSeconds(targetProbeName, baseSpeed, drivePastDistance, manipulatorState); + + try + { + while (AutomationProgressState != AutomationProgressState.AtTargetEntryCoordinate) + { + // Check for cancellation. + _insertionDriveCts.Token.ThrowIfCancellationRequested(); + + // Get fresh manipulator state for this specific manipulator (DuraDepth may change during exit). + var currentSceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var currentManipulatorState = currentSceneState.Manipulators.FirstOrDefault(m => m.Id == manipulatorId); + var duraDepth = currentManipulatorState?.DuraDepth ?? manipulatorState.DuraDepth; + + // Set state to next exiting state. + _storeService.Store.Dispatch( + SceneActions.SET_AUTOMATION_PROGRESS_STATE_TO_NEXT_EXITING, + manipulatorId + ); + + // Log set to exiting state. + LogDriveToTargetInsertion(manipulatorId, duraDepth, baseSpeed); + + // Update ETA. + Eta = ComputeEtaSeconds(targetProbeName, baseSpeed, drivePastDistance, manipulatorState); + DriveProgressPercentage = + _originalETA > 0 ? 1f - (float)Eta / _originalETA : 0f; + + // Handle exiting state. + switch (AutomationProgressState) + { + case AutomationProgressState.ExitingToDura: + // Exit back up to the Dura. + var exitToDuraResponse = await _ephysLinkService.SetDepth( + new SetDepthRequest( + manipulatorId, + duraDepth, + baseSpeed * EXIT_DRIVE_SPEED_MULTIPLIER + ) + ); + + if (EphysLinkService.HasError(exitToDuraResponse.Error)) + { + Debug.LogError( + $"Failed to exit to dura: {exitToDuraResponse.Error}" + ); + return; + } + break; + + case AutomationProgressState.ExitingToMargin: + // Reset dura offset. + _storeService.Store.Dispatch( + SceneActions.RESET_DURA_OFFSET, + manipulatorId + ); + + // Exit to the safe margin above the Dura. + var exitToMarginResponse = await _ephysLinkService.SetDepth( + new SetDepthRequest( + manipulatorId, + duraDepth - DURA_MARGIN_DISTANCE, + baseSpeed * EXIT_DRIVE_SPEED_MULTIPLIER + ) + ); + + if (EphysLinkService.HasError(exitToMarginResponse.Error)) + { + Debug.LogError( + $"Failed to exit to margin: {exitToMarginResponse.Error}" + ); + return; + } + break; + + case AutomationProgressState.ExitingToTargetEntryCoordinate: + // Drive to the target entry coordinate. + var entryPosition = ConvertInsertionAPMLDVToManipulatorPosition( + _trajectoryCoordinates.third, + manipulatorState + ); + if (entryPosition == null) + { + Debug.LogError( + "Failed to convert entry coordinate to manipulator position" + ); + return; + } + + var exitToEntryCoordinateResponse = await _ephysLinkService.SetPosition( + new SetPositionRequest( + manipulatorId, + entryPosition.Value, + AUTOMATIC_MOVEMENT_SPEED + ) + ); + + if (EphysLinkService.HasError(exitToEntryCoordinateResponse.Error)) + { + Debug.LogError( + $"Failed to exit to entry coordinate: {exitToEntryCoordinateResponse.Error}" + ); + return; + } + break; + } + + // Complete this phase. + _storeService.Store.Dispatch( + SceneActions.COMPLETE_AUTOMATION_INTERMEDIATE_PROGRESS, + manipulatorId + ); + + // Log the event. + LogDriveToTargetInsertion(manipulatorId, duraDepth, baseSpeed); + } + + // Exit complete - reset ETA and progress. + Eta = 0; + DriveProgressPercentage = 1f; + } + catch (OperationCanceledException) + { + // Stopped by user - state already handled by StopInsertionDrive. + } + finally + { + _insertionDriveCts?.Dispose(); + _insertionDriveCts = null; + } } [ICommand] - private void StopInsertionDrive() + private async void StopInsertionDrive() { - // State is updated externally by the ProbeService. - ProbeService - .StopInsertionDriveActiveProbe() - .ContinueWith(task => + // Cancel ongoing drive operation. + _insertionDriveCts?.Cancel(); + + // Send stop command to manipulator. + var stopResponse = await _ephysLinkService.Stop(ActiveManipulatorId); + + if (EphysLinkService.HasError(stopResponse)) + { + Debug.LogError($"Failed to stop: {stopResponse}"); + return; + } + + // Log stop event. + OutputLog.Log( + new[] { - // Do not proceed if the stop failed. - if (!task.Result) - return; + "Automation", + DateTime.Now.ToString(CultureInfo.InvariantCulture), + "Drive", + ActiveManipulatorId, + "Stop", + } + ); - // If the stop was successful, cancel the intermediate progress state. - _storeService.Store.Dispatch( - SceneActions.CANCEL_AUTOMATION_INTERMEDIATE_PROGRESS, - ActiveManipulatorId - ); - }); + // Cancel intermediate progress (revert state). + _storeService.Store.Dispatch( + SceneActions.CANCEL_AUTOMATION_INTERMEDIATE_PROGRESS, + ActiveManipulatorId + ); + + // Reset ETA and progress. + Eta = 0; + DriveProgressPercentage = 0f; + } + + [ICommand] + private void SetDrivePastDistance(int distance) + { + _storeService.Store.Dispatch( + SceneActions.SET_DRIVE_PAST_DISTANCE, + (ActiveManipulatorId, distance) + ); + } + + #endregion + + #region Trajectory Computation and Visualization + + /// + /// Compute the entry coordinate and trajectory for the target insertion. Also, create and update the trajectory visualization lines. + /// + /// The name of the target insertion probe. + /// + /// The computed entry coordinate in AP, ML, DV coordinates. Vector3.negativeInfinity if target is unset or computation fails. + /// + private Vector3 ComputeTargetEntryCoordinateTrajectory(string targetProbeName) + { + // Validate target probe world state exists + var targetProbeWorldState = GetProbeWorldState(targetProbeName); + if (targetProbeWorldState == null) + { + Debug.LogError($"Target probe world state is null: {targetProbeName}"); + RemoveTrajectoryLines(); + return Vector3.negativeInfinity; + } + + // Check if BrainAtlasManager is ready + if ( + BrainAtlasManager.Instance == null + || BrainAtlasManager.ActiveReferenceAtlas == null + ) + { + Debug.LogWarning("BrainAtlasManager not ready for trajectory computation"); + RemoveTrajectoryLines(); + return Vector3.negativeInfinity; + } + + // Compute entry coordinate in world space using Redux state + var surfaceCoordinateWorldT = targetProbeWorldState.SurfaceCoordinateWorldT; + var tipForwardWorldU = targetProbeWorldState.TipForwardWorldU; + + var entryCoordinateWorld = + surfaceCoordinateWorldT + - tipForwardWorldU * IDEAL_ENTRY_COORDINATE_TO_DURA_DISTANCE; + + // Convert to AP/ML/DV coordinates + var entryCoordinateAtlasU = BrainAtlasManager.ActiveReferenceAtlas.World2Atlas( + entryCoordinateWorld + ); + var entryCoordinateAPMLDV = BrainAtlasManager.ActiveAtlasTransform.U2T( + entryCoordinateAtlasU + ); + + // Get current visualization probe coordinate using ProbeController local values. + var currentCoordinateNullable = GetVisualizationProbeAPMLDV(); + if (currentCoordinateNullable == null) + { + Debug.LogError("Visualization probe not found"); + return Vector3.negativeInfinity; + } + + var currentCoordinate = currentCoordinateNullable.Value; + + // Create 3-stage trajectory + // Stage 1: DV movement (change depth only) + _trajectoryCoordinates.first = new Vector3( + currentCoordinate.x, + currentCoordinate.y, + entryCoordinateAPMLDV.z + ); + + // Stage 2: AP movement (change AP only) + _trajectoryCoordinates.second = new Vector3( + entryCoordinateAPMLDV.x, + currentCoordinate.y, + entryCoordinateAPMLDV.z + ); + + // Stage 3: ML movement (final position) + _trajectoryCoordinates.third = entryCoordinateAPMLDV; + + // Create and update trajectory lines + CreateTrajectoryLines(); + UpdateTrajectoryLines(); + + // Return final entry coordinate + return _trajectoryCoordinates.third; + } + + /// + /// Create the trajectory line game objects and line renderers (if needed). + /// + private void CreateTrajectoryLines() + { + // Exit if they already exist + if (_trajectoryLineGameObjects.ap != null) + return; + + // Create the trajectory line game objects + _trajectoryLineGameObjects = ( + new GameObject("APTrajectoryLine") { layer = 5 }, + new GameObject("MLTrajectoryLine") { layer = 5 }, + new GameObject("DVTrajectoryLine") { layer = 5 } + ); + + // Create the line renderers + _trajectoryLineRenderers = ( + _trajectoryLineGameObjects.ap.AddComponent(), + _trajectoryLineGameObjects.ml.AddComponent(), + _trajectoryLineGameObjects.dv.AddComponent() + ); + + // Apply materials + var defaultSpriteShader = Shader.Find("Sprites/Default"); + _trajectoryLineRenderers.ap.material = new Material(defaultSpriteShader) + { + color = AP_COLOR, + }; + _trajectoryLineRenderers.ml.material = new Material(defaultSpriteShader) + { + color = ML_COLOR, + }; + _trajectoryLineRenderers.dv.material = new Material(defaultSpriteShader) + { + color = DV_COLOR, + }; + + // Set line widths + _trajectoryLineRenderers.ap.startWidth = _trajectoryLineRenderers.ap.endWidth = + LINE_WIDTH; + _trajectoryLineRenderers.ml.startWidth = _trajectoryLineRenderers.ml.endWidth = + LINE_WIDTH; + _trajectoryLineRenderers.dv.startWidth = _trajectoryLineRenderers.dv.endWidth = + LINE_WIDTH; + + // Set segment counts + _trajectoryLineRenderers.ap.positionCount = NUM_SEGMENTS; + _trajectoryLineRenderers.ml.positionCount = NUM_SEGMENTS; + _trajectoryLineRenderers.dv.positionCount = NUM_SEGMENTS; + } + + /// + /// Update the trajectory line positions. + /// + private void UpdateTrajectoryLines() + { + if ( + BrainAtlasManager.Instance == null + || BrainAtlasManager.ActiveReferenceAtlas == null + ) + return; + + // Get visualization probe tip position using ProbeController local values + var tipWorldT = GetVisualizationProbeTipWorldT(); + if (tipWorldT == null) + return; + + // DV line: From current probe tip to first coordinate + _trajectoryLineRenderers.dv.SetPosition( + 0, + tipWorldT.Value + ); + _trajectoryLineRenderers.dv.SetPosition( + 1, + BrainAtlasManager.ActiveReferenceAtlas.Atlas2World( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(_trajectoryCoordinates.first) + ) + ); + + // AP line: From first to second coordinate + _trajectoryLineRenderers.ap.SetPosition( + 0, + BrainAtlasManager.ActiveReferenceAtlas.Atlas2World( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(_trajectoryCoordinates.first) + ) + ); + _trajectoryLineRenderers.ap.SetPosition( + 1, + BrainAtlasManager.ActiveReferenceAtlas.Atlas2World( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(_trajectoryCoordinates.second) + ) + ); + + // ML line: From second to third coordinate + _trajectoryLineRenderers.ml.SetPosition( + 0, + BrainAtlasManager.ActiveReferenceAtlas.Atlas2World( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(_trajectoryCoordinates.second) + ) + ); + _trajectoryLineRenderers.ml.SetPosition( + 1, + BrainAtlasManager.ActiveReferenceAtlas.Atlas2World( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(_trajectoryCoordinates.third) + ) + ); + } + + /// + /// Destroy the trajectory line game objects and line renderers. Also reset the references. + /// + private void RemoveTrajectoryLines() + { + // Destroy the objects + if (_trajectoryLineGameObjects.ap != null) + UnityEngine.Object.Destroy(_trajectoryLineGameObjects.ap); + if (_trajectoryLineGameObjects.ml != null) + UnityEngine.Object.Destroy(_trajectoryLineGameObjects.ml); + if (_trajectoryLineGameObjects.dv != null) + UnityEngine.Object.Destroy(_trajectoryLineGameObjects.dv); + + // Reset the references + _trajectoryLineGameObjects = (null, null, null); + _trajectoryLineRenderers = (null, null, null); + + // Reset trajectory coordinates + _trajectoryCoordinates = ( + Vector3.negativeInfinity, + Vector3.negativeInfinity, + Vector3.negativeInfinity + ); + } + + /// + /// Convert insertion AP, ML, DV coordinates to manipulator translation stage position. + /// + /// AP, ML, DV coordinates from an insertion. + /// Optional manipulator state to use. If null, uses active manipulator state. + /// Computed manipulator translation stage positions to match this coordinate, or null if conversion fails. + private Vector4? ConvertInsertionAPMLDVToManipulatorPosition(Vector3 insertionAPMLDV, ManipulatorState manipulatorState = null) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + + // Validate we have the necessary data + if (BrainAtlasManager.ActiveReferenceAtlas == null) + { + Debug.LogError("BrainAtlasManager.ActiveReferenceAtlas is null"); + return null; + } + + if (sceneState.ManipulatorCoordinateSpace == null) + { + Debug.LogError("ManipulatorCoordinateSpace is null"); + return null; + } + + // Convert AP/ML/DV to world coordinate + var convertToWorld = BrainAtlasManager.ActiveReferenceAtlas.Atlas2World_Vector( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(insertionAPMLDV) + ); + + // Create coordinate transform based on manipulator configuration + var numAxes = sceneState.NumberOfAxesOnManipulator; + var isRightHanded = activeManipulatorState.Handedness == ManipulatorHandedness.Right; + var yaw = activeManipulatorState.Angles.x; + var pitch = activeManipulatorState.Angles.y; + + CoordinateTransform coordinateTransform = numAxes switch + { + 4 => isRightHanded + ? new FourAxisRightHandedManipulatorTransform(yaw) + : new FourAxisLeftHandedManipulatorTransform(yaw), + 3 => new ThreeAxisLeftHandedTransform(yaw, pitch), + _ => null, + }; + + if (coordinateTransform == null) + { + Debug.LogError($"Unsupported number of axes: {numAxes}"); + return null; + } + + // Convert to manipulator space + var posInManipulatorSpace = sceneState.ManipulatorCoordinateSpace.World2Space( + convertToWorld + ); + Vector4 posInManipulatorTransform = coordinateTransform.U2T(posInManipulatorSpace); + + // Apply brain surface offset (DuraOffset) + var duraOffset = activeManipulatorState.DuraOffset; + posInManipulatorTransform.w -= float.IsNaN(duraOffset) ? 0 : duraOffset; + + // Apply coordinate offsets and return result + return posInManipulatorTransform + activeManipulatorState.ReferenceCoordinateOffset; + } + + #endregion + + #region Insertion Helper Methods + + /// + /// Get the visualization probe's current tip position in world space. + /// For visualization probes, uses ProbeController local values; otherwise uses ProbeWorldState. + /// + /// Optional manipulator state to use. If null, uses active manipulator state. + private Vector3? GetVisualizationProbeTipWorldT(ManipulatorState manipulatorState = null) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + var visualizationProbeName = activeManipulatorState.VisualizationProbeName; + + var visualizationProbeManager = ProbeManager.Instances.FirstOrDefault(m => + m.name == visualizationProbeName + ); + if (visualizationProbeManager == null) + return null; + + var probeController = visualizationProbeManager.ProbeController; + if (probeController.IsVisualizationProbe) + { + // Convert local APMLDV to world position + return BrainAtlasManager.ActiveReferenceAtlas.Atlas2World( + BrainAtlasManager.ActiveAtlasTransform.T2U_Vector( + probeController.VisualizationLocalAPMLDV + ) + ); + } + + var probeWorldState = GetProbeWorldState(visualizationProbeName); + return probeWorldState?.TipPositionWorldT; + } + + /// + /// Get the visualization probe's current APMLDV coordinates. + /// For visualization probes, uses ProbeController local values. + /// + /// Optional manipulator state to use. If null, uses active manipulator state. + private Vector3? GetVisualizationProbeAPMLDV(ManipulatorState manipulatorState = null) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + var visualizationProbeName = activeManipulatorState.VisualizationProbeName; + + var visualizationProbeManager = ProbeManager.Instances.FirstOrDefault(m => + m.name == visualizationProbeName + ); + if (visualizationProbeManager == null) + return null; + + var probeController = visualizationProbeManager.ProbeController; + return probeController.IsVisualizationProbe + ? probeController.VisualizationLocalAPMLDV + : null; + } + + /// + /// Get the visualization probe's forward direction in transformed space. + /// + /// Optional manipulator state to use. If null, uses active manipulator state. + private Vector3? GetVisualizationProbeForwardT(ManipulatorState manipulatorState = null) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + var visualizationProbeName = activeManipulatorState.VisualizationProbeName; + + var visualizationProbeManager = ProbeManager.Instances.FirstOrDefault(m => + m.name == visualizationProbeName + ); + if (visualizationProbeManager == null) + return null; + + var probeController = visualizationProbeManager.ProbeController; + return probeController.IsVisualizationProbe + ? probeController.VisualizationLocalForwardT + : probeController.ProbeTipT.forward; + } + + /// + /// Check if the current state allows starting or resuming insertion drive. + /// + private static bool IsInsertable(AutomationProgressState state) + { + return state + is AutomationProgressState.AtDuraInsert + or AutomationProgressState.AtNearTargetInsert + or AutomationProgressState.AtPastTarget + or AutomationProgressState.AtTarget; + } + + /// + /// Check if the current state allows starting or resuming exit. + /// + private static bool IsExitable(AutomationProgressState state) + { + return state + is AutomationProgressState.AtDuraInsert + or AutomationProgressState.AtNearTargetInsert + or AutomationProgressState.AtPastTarget + or AutomationProgressState.AtTarget + or AutomationProgressState.AtDuraExit + or AutomationProgressState.AtExitMargin; + } + + /// + /// Compute the target coordinate adjusted for the probe's actual position. + /// + /// Target probe name. + /// Optional manipulator state to use. If null, uses active manipulator state. + /// APMLDV coordinates of where the probe should actually go. + private Vector3 GetOffsetAdjustedTargetCoordinate(string targetProbeName, ManipulatorState manipulatorState = null) + { + // Get target probe world state from Redux (target probes are NOT visualization probes) + var targetProbeWorldState = GetProbeWorldState(targetProbeName); + if (targetProbeWorldState == null) + return Vector3.negativeInfinity; + + // Get visualization probe position and forward using ProbeController local values + var visualizationWorldT = GetVisualizationProbeTipWorldT(manipulatorState); + var probeTipForward = GetVisualizationProbeForwardT(manipulatorState); + + if (visualizationWorldT == null || probeTipForward == null) + return Vector3.negativeInfinity; + + var targetWorldT = targetProbeWorldState.TipPositionWorldT; + + var relativePositionWorldT = visualizationWorldT.Value - targetWorldT; + var offsetAdjustedRelativeTargetPositionWorldT = Vector3.ProjectOnPlane( + relativePositionWorldT, + probeTipForward.Value + ); + var offsetAdjustedTargetCoordinateWorldT = + targetWorldT + offsetAdjustedRelativeTargetPositionWorldT; + + // Convert worldT to AtlasT then switch axes to get APMLDV. + var offsetAdjustedTargetCoordinateAtlasT = + BrainAtlasManager.ActiveReferenceAtlas.World2Atlas( + offsetAdjustedTargetCoordinateWorldT + ); + return BrainAtlasManager.ActiveAtlasTransform.U2T_Vector( + offsetAdjustedTargetCoordinateAtlasT + ); + } + + /// + /// Compute the absolute distance from the target insertion to the Dura. + /// + /// Target probe name to compute distance to. + /// Optional manipulator state to use. If null, uses active manipulator state. + /// Distance in mm to the target from the Dura. + private float GetTargetDistanceToDura(string targetProbeName, ManipulatorState manipulatorState = null) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + var duraCoordinate = activeManipulatorState.DuraCoordinate; + return Vector3.Distance( + GetOffsetAdjustedTargetCoordinate(targetProbeName, manipulatorState), + new Vector3(duraCoordinate.x, duraCoordinate.y, duraCoordinate.z) + ); + } + + /// + /// Compute the current distance to the target insertion. + /// + /// Target probe name. + /// Optional manipulator state to use. If null, uses active manipulator state. + /// Distance in mm to the target from the probe. + private float GetCurrentDistanceToTarget(string targetProbeName, ManipulatorState manipulatorState = null) + { + // Get visualization probe APMLDV using ProbeController local values + var visualizationAPMLDV = GetVisualizationProbeAPMLDV(manipulatorState); + if (visualizationAPMLDV == null) + return float.NaN; + + return Vector3.Distance( + visualizationAPMLDV.Value, + GetOffsetAdjustedTargetCoordinate(targetProbeName, manipulatorState) + ); + } + + /// + /// Compute the target depth for the probe to drive to. + /// + /// Target probe name to drive (insert) to. + /// Optional manipulator state to use. If null, uses active manipulator state. + /// The depth the manipulator needs to drive to reach the target insertion. + private float GetTargetDepth(string targetProbeName, ManipulatorState manipulatorState = null) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + var duraDepth = activeManipulatorState.DuraDepth; + + // DuraOffset is for visualization only - do not apply it to manipulator depth calculations + return duraDepth + GetTargetDistanceToDura(targetProbeName, manipulatorState); + } + + /// + /// Compute the ETA in seconds for a probe to reach a target insertion (or exit). + /// + /// Target probe name to calculate ETA to. + /// Base driving speed in mm/s. + /// Distance to drive past target in mm. + /// Optional manipulator state to use. If null, uses active manipulator state. + /// ETA in seconds for reaching a target or exiting. + private int ComputeEtaSeconds( + string targetProbeName, + float baseSpeed, + float drivePastDistance, + ManipulatorState manipulatorState = null + ) + { + var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE); + var activeManipulatorState = manipulatorState ?? sceneState.ActiveManipulatorState; + + var distanceToTarget = GetCurrentDistanceToTarget(targetProbeName, manipulatorState); + var targetDistanceToDura = GetTargetDistanceToDura(targetProbeName, manipulatorState); + var actualExitMarginToDuraDistance = activeManipulatorState.DuraDepth - _entryCoordinateDepth; + + var secondsToDestination = AutomationProgressState switch + { + AutomationProgressState.DrivingToNearTarget => Mathf.Max( + 0, + distanceToTarget - NEAR_TARGET_DISTANCE + ) / baseSpeed + + (NEAR_TARGET_DISTANCE + 2 * drivePastDistance) + / (baseSpeed * NEAR_TARGET_SPEED_MULTIPLIER), + AutomationProgressState.DrivingToPastTarget => ( + distanceToTarget + 2 * drivePastDistance + ) / (baseSpeed * NEAR_TARGET_SPEED_MULTIPLIER), + AutomationProgressState.ReturningToTarget => distanceToTarget + / (baseSpeed * NEAR_TARGET_SPEED_MULTIPLIER), + AutomationProgressState.ExitingToDura => (targetDistanceToDura - distanceToTarget) + / (baseSpeed * EXIT_DRIVE_SPEED_MULTIPLIER) + + DURA_MARGIN_DISTANCE / (baseSpeed * EXIT_DRIVE_SPEED_MULTIPLIER) + + actualExitMarginToDuraDistance / AUTOMATIC_MOVEMENT_SPEED, + AutomationProgressState.ExitingToMargin => ( + DURA_MARGIN_DISTANCE - (distanceToTarget - targetDistanceToDura) + ) / (baseSpeed * EXIT_DRIVE_SPEED_MULTIPLIER) + + actualExitMarginToDuraDistance / AUTOMATIC_MOVEMENT_SPEED, + AutomationProgressState.ExitingToTargetEntryCoordinate => ( + IDEAL_ENTRY_COORDINATE_TO_DURA_DISTANCE + - (distanceToTarget - targetDistanceToDura) + ) / AUTOMATIC_MOVEMENT_SPEED, + _ => 0, + }; + + return (int)secondsToDestination; + } + + /// + /// Log a drive event. + /// + /// ID of the manipulator being driven. + /// Target depth of drive. + /// Base speed of drive. + /// Distance (mm) driven past the target. + private void LogDriveToTargetInsertion( + string manipulatorId, + float targetDepth, + float baseSpeed, + float drivePastDistance = 0 + ) + { + OutputLog.Log( + new[] + { + "Automation", + DateTime.Now.ToString(CultureInfo.InvariantCulture), + "DriveToTargetInsertion", + manipulatorId, + AutomationProgressState.ToString(), + (targetDepth * 1000).ToString(CultureInfo.InvariantCulture), + (baseSpeed * 1000).ToString(CultureInfo.InvariantCulture), + (drivePastDistance * 1000).ToString(CultureInfo.InvariantCulture), + } + ); } #endregion diff --git a/Assets/Scripts/UI/Views/AutomationInspectorView.cs b/Assets/Scripts/UI/Views/AutomationInspectorView.cs index 9cca5824..002fd089 100644 --- a/Assets/Scripts/UI/Views/AutomationInspectorView.cs +++ b/Assets/Scripts/UI/Views/AutomationInspectorView.cs @@ -116,9 +116,16 @@ public AutomationInspectorView(AutomationInspectorViewModel automationInspectorV var targetEntryDriveButton = root.Q