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)