@@ -121,6 +121,23 @@ public static Bvv getInstance() {
121121 // converted to a world-space distance via pickRadiusWorld() so it stays perceptually constant across zoom levels
122122 private static final double FORK_POINT_PICK_RADIUS_SCREEN_PX = 50 ;
123123
124+ /* Tracing-click recenter gating (see registerCenterOnDoubleClickListener) */
125+ // Set to false to recenter scene on every tracing click. The gating below is for data still streaming
126+ // from disk/network, where recentering on every click may race against tile loading and producing
127+ // zig-zagged paths (most visible away from the focal plane, where perspective error is largest)
128+ private boolean gatedTracingRecenter = true ;
129+ // Fraction of nearClip below which a clicked point is considered "close enough" to the focal plane
130+ // that recentering would add nothing but disruption (an animated transform change plus a settle wait)
131+ // Expressed as a fraction of nearClip (see BvvUtils#computeCamParams)
132+ private static final double RECENTER_SKIP_FRACTION_OF_NEAR_CLIP = 0.3 ;
133+ // Extra buffer (beyond the recenter animation's own duration) given to BVV's tile streaming to catch
134+ // up with the new focal region before ray-max sampling is trusted again
135+ private static final long RECENTER_SETTLE_BUFFER_MS = 150 ;
136+ // Wall-clock time (System.currentTimeMillis()) until which trace clicks are deferred because a recenter
137+ // animation is either still running or has just finished (tiles for the new region may still be
138+ // streaming in). 0 (or already elapsed) means the scene is considered settled
139+ private volatile long tracingSettleUntilMs = 0 ;
140+
124141 private MouseEvent lastPositionEvent ;
125142 private double [] lastPosition ;
126143
@@ -1542,6 +1559,11 @@ private File getSnapshotFile(final String directory) throws IOException {
15421559 * world point maps to the screen center at viewerZ = 0 (the focal plane).
15431560 * This forces BVV to load tiles at the clicked location, making subsequent
15441561 * findClickRayMaxima() calls accurate.
1562+ * <p>
1563+ * While tracing, this fires on every single click (not just double-clicks), so on
1564+ * data still streaming from disk/network the animated recenter may race against tile
1565+ * loading: the very next click can land mid-animation, against tiles that are still
1566+ * catching up to the *previous* recenter. See {@link #gatedTracingRecenter}
15451567 */
15461568 private void registerCenterOnDoubleClickListener (final BigVolumeViewer bvv ) {
15471569 bvv .getViewer ().getDisplay ().getComponent ().addMouseListener (
@@ -1580,6 +1602,19 @@ public void mouseClicked(final java.awt.event.MouseEvent e) {
15801602 vp .state ().getViewerTransform (current );
15811603 final double [] viewerPt = new double [3 ];
15821604 current .apply (target , viewerPt );
1605+
1606+ if (reactTracingEnabled && gatedTracingRecenter ) {
1607+ // Skip the recenter entirely when the click already lands close enough to the
1608+ // focal plane that foreshortening error is negligible. This is the common case
1609+ // once a path settles near the focal plane, and skipping it avoids disrupting
1610+ // fluid tracing on every single click
1611+ final double nearClip = (pathOverlay != null ) ? pathOverlay .overlayRenderer .nearClip
1612+ : BvvUtils .DEFAULT_NEAR_CLIP ;
1613+ if (Math .abs (viewerPt [2 ]) < RECENTER_SKIP_FRACTION_OF_NEAR_CLIP * nearClip ) {
1614+ return ;
1615+ }
1616+ }
1617+
15831618 final double cx = vp .getDisplay ().getWidth () / 2.0 ;
15841619 final double cy = vp .getDisplay ().getHeight () / 2.0 ;
15851620 final AffineTransform3D dest = current .copy ();
@@ -1588,6 +1623,23 @@ public void mouseClicked(final java.awt.event.MouseEvent e) {
15881623 dest .set (current .get (2 , 3 ) + (0 - viewerPt [2 ]), 2 , 3 );
15891624 // Calling setTransformAnimator replaces any running animation immediately (interruptible)
15901625 vp .setTransformAnimator (new SimilarityTransformAnimator (current , dest , 0 , 0 , CENTER_ANIMATION_DURATION_MS ));
1626+
1627+ if (reactTracingEnabled && gatedTracingRecenter ) {
1628+ // Defer trace-click acceptance until the animation finishes and tiles for the
1629+ // new focal region have had a chance to stream in (see Tracer#mouseClicked)
1630+ final long settleDelayMs = CENTER_ANIMATION_DURATION_MS + RECENTER_SETTLE_BUFFER_MS ;
1631+ tracingSettleUntilMs = System .currentTimeMillis () + settleDelayMs ;
1632+ setTracingStatus (true , "Stabilizing..." );
1633+ // Clears the status once the *latest* settle window has elapsed. If another
1634+ // recenter fires before this runs, tracingSettleUntilMs will have been pushed
1635+ // further out and this no-ops, leaving it to the newer timer to clear instead
1636+ final long expectedSettle = tracingSettleUntilMs ;
1637+ final javax .swing .Timer settleTimer = new javax .swing .Timer ((int ) settleDelayMs , ev -> {
1638+ if (tracingSettleUntilMs == expectedSettle ) setTracingStatus (false , null );
1639+ });
1640+ settleTimer .setRepeats (false );
1641+ settleTimer .start ();
1642+ }
15911643 }
15921644 });
15931645 }
@@ -3202,6 +3254,16 @@ public void mouseClicked(final MouseEvent e) {
32023254 }
32033255 if (computing ) return ; // a segment is still being traced; ignore further clicks until it lands
32043256
3257+ if (gatedTracingRecenter && System .currentTimeMillis () < tracingSettleUntilMs ) {
3258+ // A recenter animation triggered by a previous click is still running, or has just
3259+ // finished but tiles for the new focal region may still be streaming in (see
3260+ // registerCenterOnDoubleClickListener). Sampling now risks the exact zig-zag this gate
3261+ // exists to prevent, so the click is dropped rather than silently placing a bad node;
3262+ // the status row (set by the recenter listener) already reads "Stabilizing..."
3263+ showViewerMessage ("Scene still stabilizing, please click again" );
3264+ return ;
3265+ }
3266+
32053267 if (previousNode == null ) {
32063268 // this is the first click: we are starting a new Path. Lock in the channel/frame (and, for A*,
32073269 // the pixel data) for the whole path now, rather than re-checking on every segment, so a single
0 commit comments