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