diff --git a/.gitignore b/.gitignore
index 4481bae5..3fdbaad5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,4 @@ Assets/StreamingAssets/EphysLink-*
Assets/Samples/*
Assets/settings_conversion.txt
.github/copilot-instructions.md
+.nuget/
diff --git a/Assets/Materials/Volume/InPlaneSliceMaterialBlit.mat b/Assets/Materials/Volume/InPlaneSliceMaterialBlit.mat
index 789843f0..334ef106 100644
--- a/Assets/Materials/Volume/InPlaneSliceMaterialBlit.mat
+++ b/Assets/Materials/Volume/InPlaneSliceMaterialBlit.mat
@@ -148,8 +148,8 @@ Material:
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _ForwardDirection: {r: 0, g: 0, b: 0, a: 0}
- _LeftDirection: {r: 1, g: 0, b: 0, a: 0}
- - _RecordingRegionCenterPosition: {r: 52.480907, g: 228, b: 145.78673, a: 0}
- - _RightDirection: {r: 1, g: 0, b: 0, a: 0}
+ - _RecordingRegionCenterPosition: {r: 328, g: 85.79999, b: 100.18744, a: 0}
+ - _RightDirection: {r: -0.00000023841858, g: 0, b: -1, a: 0}
- _Scale: {r: 200, g: 200, b: 4.5, a: 0}
- _SpecColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
- _TipPosition: {r: 0, g: 0, b: 0, a: 0}
diff --git a/Assets/Scripts/Models/Scene/ManipulatorState.cs b/Assets/Scripts/Models/Scene/ManipulatorState.cs
index 49aef4f1..f4a076e7 100644
--- a/Assets/Scripts/Models/Scene/ManipulatorState.cs
+++ b/Assets/Scripts/Models/Scene/ManipulatorState.cs
@@ -77,6 +77,11 @@ public record ManipulatorState
public Vector4 DemoTargetCoordinate;
+ ///
+ /// Indicates if the demo mode is currently running.
+ ///
+ /// Not saved. Will always start as off.
+ [NonSerialized]
public bool IsDemoRunning;
#endregion
diff --git a/Assets/Scripts/Pinpoint/Probes/Controllers/CartesianProbeController.cs b/Assets/Scripts/Pinpoint/Probes/Controllers/CartesianProbeController.cs
index defdac97..4e13fdfc 100644
--- a/Assets/Scripts/Pinpoint/Probes/Controllers/CartesianProbeController.cs
+++ b/Assets/Scripts/Pinpoint/Probes/Controllers/CartesianProbeController.cs
@@ -310,6 +310,9 @@ private void Start()
private void Update()
{
+ // Update visualization probe position if this is a visualization probe.
+ UpdateVisualizationProbePosition();
+
// If the user is holding one or more click keys and we are past the hold delay, increment the position
if (clickKeyHeld > 0 && (Time.realtimeSinceStartup - clickKeyPressTime) > keyHoldDelay)
// Set speed to Tap instead of Hold for manipulator keyboard control
@@ -351,8 +354,8 @@ private void OnDestroy()
private void OnProbeStateChanged(ProbeState state)
{
- // Skip if no state.
- if (state == null)
+ // Skip if is visualization probe (updates from local state) no state.
+ if (IsVisualizationProbe || state == null)
{
return;
}
@@ -918,6 +921,38 @@ public void DragMovementRelease()
#region Set Probe pos/angles
+ ///
+ /// Apply visualization probe updates if available.
+ /// This should be called from the Update() method of concrete ProbeController implementations.
+ ///
+ private void UpdateVisualizationProbePosition()
+ {
+ if (!IsVisualizationProbe)
+ return;
+
+ // Update position.
+ transform.position =
+ BrainAtlasManager.ActiveReferenceAtlas.Atlas2World(
+ BrainAtlasManager.ActiveAtlasTransform.T2U_Vector(VisualizationLocalAPMLDV)
+ );
+
+
+ // Update orientation.
+ transform.rotation = _initialRotation;
+ transform.RotateAround(_probeTipT.position, transform.up, VisualizationLocalAngles.x);
+ transform.RotateAround(_probeTipT.position, transform.right, VisualizationLocalAngles.y);
+ transform.RotateAround(_probeTipT.position, transform.forward, -VisualizationLocalAngles.z);
+
+ // Update tip coords.
+ SetTipWorldU();
+
+ // Update recording region info.
+ ProbeManager.ProbeMoved();
+
+ // Update surface coordinates.
+ ProbeManager.UpdateSurfacePosition();
+ }
+
///
/// Set the probe position to the current apml/depth/angles values
///
diff --git a/Assets/Scripts/Pinpoint/Probes/ProbeController.cs b/Assets/Scripts/Pinpoint/Probes/ProbeController.cs
index 3c9c1689..f31de19c 100644
--- a/Assets/Scripts/Pinpoint/Probes/ProbeController.cs
+++ b/Assets/Scripts/Pinpoint/Probes/ProbeController.cs
@@ -28,6 +28,36 @@ public void Register(ProbeManager probeManager)
public bool ManipulatorManualControl;
public bool ManipulatorKeyboardMoveInProgress;
+ #region Visualization Probe Local Position
+
+ ///
+ /// Local field for visualization probe position updates from EphysLink.
+ /// Used to avoid excessive Redux state updates during rapid polling.
+ ///
+ public Vector3 VisualizationLocalAPMLDV { get; set; }
+
+ ///
+ /// Local field for visualization probe depth.
+ ///
+ public float VisualizationLocalDepth { get; set; }
+
+ ///
+ /// Local field for visualization probe angles.
+ ///
+ public Vector3 VisualizationLocalAngles { get; set; }
+
+ ///
+ /// Local field for visualization probe forward vector.
+ ///
+ public Vector3 VisualizationLocalForwardT { get; set; }
+
+ ///
+ /// Indicates if new visualization data is available and needs to be applied.
+ ///
+ public bool IsVisualizationProbe { get; set; }
+
+ #endregion
+
public abstract Transform ProbeTipT { get; }
public abstract (Vector3 tipCoordWorldU, Vector3 tipRightWorldU, Vector3 tipUpWorldU, Vector3 tipForwardWorldU) GetTipWorldU();
@@ -69,4 +99,5 @@ public void SetSpaceTransform(CoordinateSpace atlas, CoordinateTransform transfo
}
+
}
diff --git a/Assets/Scripts/Services/EphysLinkService.cs b/Assets/Scripts/Services/EphysLinkService.cs
index df6a84e9..257a6f43 100644
--- a/Assets/Scripts/Services/EphysLinkService.cs
+++ b/Assets/Scripts/Services/EphysLinkService.cs
@@ -3,6 +3,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using BestHTTP.SocketIO3;
using BrainAtlas;
@@ -19,7 +20,6 @@
using Utils;
using Utils.Types;
using Action = System.Action;
-using System.Threading;
namespace Services
{
@@ -34,7 +34,6 @@ public class EphysLinkService
// Visualization loop interval (ms)
private const int VISUALIZATION_UPDATE_INTERVAL_MS = 10; // about 60 Hz
-
#endregion
#region Services
@@ -52,15 +51,16 @@ public class EphysLinkService
public string SocketId => _socket.Id;
- // Cancellation for the visualization update loop
- private CancellationTokenSource _visualizationLoopCts;
-
#endregion
#region Demo Loop
private readonly HashSet _runningDemoLoops = new();
private const float DEMO_SPEED = 0.5f; // mm/s
+
+ // Cancellation for the visualization update loop
+ private CancellationTokenSource _visualizationLoopCts;
+
#endregion
public EphysLinkService(StoreService storeService)
@@ -169,7 +169,9 @@ async Task OnConnectedAsync()
}
catch (Exception ex)
{
- HandleError($"{GetErrorConnectingToServerMessage()} Caused exception: {ex.Message}");
+ HandleError(
+ $"{GetErrorConnectingToServerMessage()} Caused exception: {ex.Message}"
+ );
}
}
@@ -189,7 +191,10 @@ async Task OnConnectedAsync()
// On successful connection, delegate to async task handler.
_socket.Once(
"connect",
- () => { _ = OnConnectedAsync(); }
+ () =>
+ {
+ _ = OnConnectedAsync();
+ }
);
// On error.
@@ -561,6 +566,10 @@ private static string ToJson(T data)
return JsonUtility.ToJson(data);
}
+ #endregion
+
+ #region Visualization control
+
// Starts the continuous visualization update loop until the socket disconnects or service disconnects.
private void StartVisualizationLoop()
{
@@ -573,17 +582,21 @@ private void StartVisualizationLoop()
// Cancels and disposes the visualization update loop.
private void StopVisualizationLoop()
{
- if (_visualizationLoopCts != null)
+ if (_visualizationLoopCts == null)
+ return;
+ try
{
- try { _visualizationLoopCts.Cancel(); }
- catch (ObjectDisposedException) { /* ignore disposed */ }
- catch (Exception ex)
- {
- Debug.Log($"Ignored exception during visualization loop cancellation: {ex}");
- }
- _visualizationLoopCts.Dispose();
- _visualizationLoopCts = null;
+ _visualizationLoopCts.Cancel();
}
+ catch (ObjectDisposedException)
+ { /* ignore disposed */
+ }
+ catch (Exception ex)
+ {
+ Debug.Log($"Ignored exception during visualization loop cancellation: {ex}");
+ }
+ _visualizationLoopCts.Dispose();
+ _visualizationLoopCts = null;
}
// The loop body calling UpdateVisualizationProbePosition at a fixed interval.
@@ -593,7 +606,9 @@ private async Task VisualizationUpdateLoop(CancellationToken token)
{
try
{
- var sceneState = _storeService.Store.GetState(SliceNames.SCENE_SLICE);
+ var sceneState = _storeService.Store.GetState(
+ SliceNames.SCENE_SLICE
+ );
await UpdateVisualizationProbePosition(sceneState);
}
catch (OperationCanceledException)
@@ -616,21 +631,8 @@ private async Task VisualizationUpdateLoop(CancellationToken token)
}
}
- #endregion
-
- #region Visualization control
-
private async Task UpdateVisualizationProbePosition(SceneState sceneState)
{
- List<(
- string Name,
- Vector3 SurfaceAPMLDV,
- float Depth,
- Vector3 ForwardT,
- Vector3 Angles,
- Vector2 PitchRange
- )> requests = new();
-
foreach (
var manipulatorState in sceneState.Manipulators.Where(state =>
!string.IsNullOrEmpty(state.VisualizationProbeName)
@@ -647,12 +649,12 @@ var manipulatorState in sceneState.Manipulators.Where(state =>
) == null
|| visualizationProbeManager == null
)
- return;
+ continue;
// Get the current position of the manipulator.
var positionResponse = await GetPosition(manipulatorState.Id);
if (HasError(positionResponse.Error))
- return;
+ continue;
// Apply reference coordinate offset.
var referenceCoordinateAdjustedManipulatorPosition =
@@ -700,13 +702,16 @@ var manipulatorState in sceneState.Manipulators.Where(state =>
var transformedAPMLDV = BrainAtlasManager.World2T_Vector(
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)
- {
+ 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(
@@ -715,46 +720,29 @@ var manipulatorState in sceneState.Manipulators.Where(state =>
)
);
- switch (sceneState.NumberOfAxesOnManipulator)
+ // Get the ProbeController for this visualization probe
+ var probeController = visualizationProbeManager.ProbeController;
+ if (probeController == null)
+ continue;
+
+ var depth = sceneState.NumberOfAxesOnManipulator switch
{
- // Set the probe position in the store.
- case 3:
- requests.Add(
- (
- manipulatorState.VisualizationProbeName,
- transformedAPMLDV,
- duraOffsetAdjustment,
- forwardT,
- manipulatorState.Angles,
- _pitchRange
- )
- );
- break;
- case 4:
- requests.Add(
- (
- manipulatorState.VisualizationProbeName,
- transformedAPMLDV,
- referenceCoordinateAdjustedManipulatorPosition.w,
- forwardT,
- manipulatorState.Angles,
- _pitchRange
- )
- );
- break;
- default:
- throw new ValueOutOfRangeException(
- "Number of axes on manipulator is invalid."
- );
- }
- }
+ 3 => duraOffsetAdjustment,
+ 4 => referenceCoordinateAdjustedManipulatorPosition.w,
+ _ => throw new ValueOutOfRangeException(
+ "Number of axes on manipulator is invalid."
+ ),
+ };
- // Dispatch all position updates in one go (if any).
- if (requests.Any())
- _storeService.Store.Dispatch(
- SceneActions.BULK_SET_PROBE_POSITION_AND_ANGLES_BY,
- requests
- );
+ // Write position data directly to ProbeController's local fields
+ probeController.VisualizationLocalAPMLDV = transformedAPMLDV;
+ probeController.VisualizationLocalDepth = depth;
+ probeController.VisualizationLocalAngles = manipulatorState.Angles;
+ probeController.VisualizationLocalForwardT = forwardT;
+
+ // Mark probe as visualization probe to apply new data in Update().
+ probeController.IsVisualizationProbe = true;
+ }
}
public async Task SetManipulatorReferenceCoordinateToCurrentPosition(string manipulatorId)