@@ -2078,6 +2078,104 @@ private java.util.List<Tile> chooseSpreadTiles(java.util.List<Tile> tiles, int n
20782078 return result ;
20792079 }
20802080
2081+ /**
2082+ * Orders the chosen autofocus tiles into a short stage route to minimize XY travel. Uses a
2083+ * greedy nearest-neighbor tour seeded at the current stage position (so the first hop is short
2084+ * too), then a 2-opt cleanup pass to remove the obvious crossings the greedy pass leaves. Point
2085+ * counts here are small (a handful per run), so the O(n^2)/O(n^3) cost is negligible. On
2086+ * multi-well plates the chosen points are already spatially clustered per well, so nearest-
2087+ * neighbor naturally finishes one well before crossing to the next.
2088+ */
2089+ private java .util .List <Tile > orderForTravel (java .util .List <Tile > chosen ) {
2090+ if (chosen .size () <= 2 ) {
2091+ return chosen ;
2092+ }
2093+ java .util .List <Tile > remaining = new java .util .ArrayList <>(chosen );
2094+
2095+ // Seed at the tile nearest the current stage position when it is readable, else the first.
2096+ double curX = 0 ;
2097+ double curY = 0 ;
2098+ boolean haveCur = false ;
2099+ try {
2100+ String xy = studio_ .core ().getXYStageDevice ();
2101+ if (xy != null && !xy .isEmpty ()) {
2102+ curX = studio_ .core ().getXPosition (xy );
2103+ curY = studio_ .core ().getYPosition (xy );
2104+ haveCur = true ;
2105+ }
2106+ } catch (Exception ignore ) {
2107+ // Stage position unavailable -- start from the first chosen tile.
2108+ }
2109+ Tile start = remaining .get (0 );
2110+ if (haveCur ) {
2111+ double best = Double .MAX_VALUE ;
2112+ for (Tile t : remaining ) {
2113+ double d = dist2 (t .stageX , t .stageY , curX , curY );
2114+ if (d < best ) {
2115+ best = d ;
2116+ start = t ;
2117+ }
2118+ }
2119+ }
2120+ remaining .remove (start );
2121+ java .util .List <Tile > route = new java .util .ArrayList <>(chosen .size ());
2122+ route .add (start );
2123+
2124+ // Greedy nearest-neighbor.
2125+ while (!remaining .isEmpty ()) {
2126+ Tile last = route .get (route .size () - 1 );
2127+ Tile next = remaining .get (0 );
2128+ double best = Double .MAX_VALUE ;
2129+ for (Tile t : remaining ) {
2130+ double d = dist2 (last .stageX , last .stageY , t .stageX , t .stageY );
2131+ if (d < best ) {
2132+ best = d ;
2133+ next = t ;
2134+ }
2135+ }
2136+ remaining .remove (next );
2137+ route .add (next );
2138+ }
2139+
2140+ twoOpt (route );
2141+ return route ;
2142+ }
2143+
2144+ /** In-place 2-opt improvement of an open tour (route start/end are not joined). */
2145+ private void twoOpt (java .util .List <Tile > route ) {
2146+ int n = route .size ();
2147+ boolean improved = true ;
2148+ while (improved ) {
2149+ improved = false ;
2150+ for (int i = 0 ; i < n - 1 ; i ++) {
2151+ for (int k = i + 1 ; k < n ; k ++) {
2152+ // Reversing route[i..k] changes only the edges entering i and leaving k.
2153+ double before = (i > 0 ? edge (route , i - 1 , i ) : 0 )
2154+ + (k < n - 1 ? edge (route , k , k + 1 ) : 0 );
2155+ double after = (i > 0 ? edge (route , i - 1 , k ) : 0 )
2156+ + (k < n - 1 ? edge (route , i , k + 1 ) : 0 );
2157+ if (after + 1e-6 < before ) {
2158+ java .util .Collections .reverse (route .subList (i , k + 1 ));
2159+ improved = true ;
2160+ }
2161+ }
2162+ }
2163+ }
2164+ }
2165+
2166+ /** Euclidean stage distance between route entries a and b. */
2167+ private double edge (java .util .List <Tile > route , int a , int b ) {
2168+ Tile ta = route .get (a );
2169+ Tile tb = route .get (b );
2170+ return Math .sqrt (dist2 (ta .stageX , ta .stageY , tb .stageX , tb .stageY ));
2171+ }
2172+
2173+ private static double dist2 (double x1 , double y1 , double x2 , double y2 ) {
2174+ double dx = x1 - x2 ;
2175+ double dy = y1 - y2 ;
2176+ return dx * dx + dy * dy ;
2177+ }
2178+
20812179 /** Farthest-point sampling of {@code count} tiles from {@code group} (Euclidean stage XY). */
20822180 private java .util .List <Tile > farthestPointSample (java .util .List <Tile > group , int count ) {
20832181 java .util .List <Tile > chosen = new java .util .ArrayList <>();
@@ -2855,18 +2953,25 @@ public void startRefineZAutomatic(int nPoints, String afMethodName, boolean with
28552953 if (edgeMargin > 0 ) {
28562954 java .util .List <Tile > interior = excludeEdgeTiles (tiles , edgeMargin );
28572955 // Only apply the exclusion if it leaves something to focus on; otherwise the grid is
2858- // too thin for the requested margin and we fall back to the full set.
2956+ // too thin for the requested margin and we fall back to the full set. Warn modally so
2957+ // the fallback is not lost under the "Refining Z..."/progress status updates that follow.
28592958 if (!interior .isEmpty ()) {
28602959 tiles = interior ;
28612960 } else {
2862- setRefineZStatus ("Edge margin too large for this grid; using all tiles." );
2961+ JOptionPane .showMessageDialog (refineZFrame_ != null ? refineZFrame_ : frame_ ,
2962+ "The edge margin of " + edgeMargin + " tile(s) excludes every tile in this "
2963+ + "grid.\n Refine Z will use all tiles instead." ,
2964+ "Refine Z" , JOptionPane .WARNING_MESSAGE );
28632965 }
28642966 }
28652967 java .util .List <Tile > chosen = chooseSpreadTiles (tiles , nPoints );
28662968 if (chosen .isEmpty ()) {
28672969 setRefineZStatus ("No tiles to refine." );
28682970 return ;
28692971 }
2972+ // The spread-sampling order is travel-arbitrary; reorder into a short stage route so
2973+ // autofocus visits points with minimal XY motion.
2974+ chosen = orderForTravel (chosen );
28702975 setRefineZRunningUi (true );
28712976 setRefineZStatus ("Refining Z..." );
28722977 refineZWorker_ = new RefineZWorker (chosen , zStages , afMethodName );
0 commit comments