3131import sc .fiji .snt .gui .GuiUtils ;
3232import sc .fiji .snt .gui .IconFactory ;
3333import sc .fiji .snt .tracing .SearchInterface ;
34+ import sc .fiji .snt .util .BoundingBox ;
3435import sc .fiji .snt .util .PointInImage ;
3536import sc .fiji .snt .util .SNTColor ;
3637import sc .fiji .snt .util .SNTPoint ;
@@ -375,6 +376,50 @@ protected static void addToCardPanelBottom(final bdv.ui.CardPanel cardPanel, fin
375376 */
376377 public abstract void setCurrentTimepoint (int timepoint );
377378
379+ /**
380+ * Animates the camera to frame the given world-coordinate bounding box: an isotropic scale is
381+ * computed so the box's width/height fit the viewport, and the transform is translated to
382+ * center the box's centroid on screen. Any existing rotation is dropped.
383+ * <p>
384+ * A degenerate box (zero width/height - e.g. a single-node selection, whose origin and
385+ * originOpposite are the same point) cannot be "fit" (there is nothing to scale to), so this
386+ * recenters on the centroid at the viewer's <em>current</em> zoom level instead.
387+ *
388+ * @param box the world-coordinate bounding box to frame; a no-op if {@code null}, its corners
389+ * are not real (e.g. an empty/uncomputed box), or the viewport has not yet been realized
390+ * @return {@code true} if the transform was computed and applied; {@code false} otherwise
391+ */
392+ public boolean flyTo (final BoundingBox box ) {
393+ if (box == null || !box .origin ().isReal () || !box .originOpposite ().isReal ()) return false ;
394+ final int cw = getViewerWidth (), ch = getViewerHeight ();
395+ if (cw <= 0 || ch <= 0 ) return false ;
396+ final double physW = box .width (), physH = box .height ();
397+ final double scale ;
398+ if (physW > 0 && physH > 0 ) {
399+ scale = Math .min (cw / physW , ch / physH );
400+ } else {
401+ // Degenerate (point-like) box: can't compute a fit scale, so keep the current zoom
402+ // and just recenter. Column norm rather than a raw matrix entry so this is correct
403+ // even if the current transform happens to carry rotation.
404+ final AffineTransform3D current = getViewerTransform ();
405+ final double currentScale = Math .sqrt (
406+ current .get (0 , 0 ) * current .get (0 , 0 ) +
407+ current .get (1 , 0 ) * current .get (1 , 0 ) +
408+ current .get (2 , 0 ) * current .get (2 , 0 ));
409+ scale = (currentScale > 0 && Double .isFinite (currentScale )) ? currentScale : 1 ;
410+ }
411+ final SNTPoint centroid = box .getCentroid ();
412+ final AffineTransform3D target = new AffineTransform3D ();
413+ target .set (scale , 0 , 0 );
414+ target .set (scale , 1 , 1 );
415+ target .set (scale , 2 , 2 );
416+ target .set (cw / 2.0 - scale * centroid .getX (), 0 , 3 );
417+ target .set (ch / 2.0 - scale * centroid .getY (), 1 , 3 );
418+ target .set (-scale * centroid .getZ (), 2 , 3 );
419+ setViewerTransform (target , 300 );
420+ return true ;
421+ }
422+
378423 /**
379424 * Creates and returns a new {@link sc.fiji.snt.BookmarkManager} for this viewer.
380425 * Called exactly once (lazily) by {@link #getMarkerManager()}.
@@ -668,8 +713,6 @@ public void actionPerformed(final ActionEvent e) {
668713 showViewerMessage ("No source selected" );
669714 return ;
670715 }
671- final int cw = getViewerWidth (), ch = getViewerHeight ();
672- if (cw <= 0 || ch <= 0 ) return ;
673716 final AffineTransform3D srcToWorld = new AffineTransform3D ();
674717 src .getSpimSource ().getSourceTransform (0 , 0 , srcToWorld );
675718 final net .imglib2 .RandomAccessibleInterval <?> rai = src .getSpimSource ().getSource (0 , 0 );
@@ -695,17 +738,10 @@ public void actionPerformed(final ActionEvent e) {
695738 minZ = Math .min (minZ , world [2 ]);
696739 maxZ = Math .max (maxZ , world [2 ]);
697740 }
698- final double physW = maxX - minX , physH = maxY - minY , physZ = maxZ - minZ ;
699- if (physW <= 0 || physH <= 0 ) return ;
700- final double scale = Math .min (cw / physW , ch / physH );
701- final AffineTransform3D target = new AffineTransform3D ();
702- target .set (scale , 0 , 0 );
703- target .set (scale , 1 , 1 );
704- target .set (scale , 2 , 2 );
705- target .set (cw / 2.0 - scale * (minX + physW / 2.0 ), 0 , 3 );
706- target .set (ch / 2.0 - scale * (minY + physH / 2.0 ), 1 , 3 );
707- target .set (-scale * (minZ + physZ / 2.0 ), 2 , 3 );
708- setViewerTransform (target , 300 );
741+ final BoundingBox box = new BoundingBox ();
742+ box .setOrigin (new PointInImage (minX , minY , minZ ));
743+ box .setOriginOpposite (new PointInImage (maxX , maxY , maxZ ));
744+ flyTo (box ); // silently no-ops if the viewport isn't realized yet or the box is degenerate
709745 }
710746 };
711747 }
0 commit comments