@@ -2025,14 +2025,26 @@ private AbstractSearch createSearch(final double world_x_start,
20252025 final double world_x_end ,
20262026 final double world_y_end ,
20272027 final double world_z_end )
2028+ {
2029+ return createSearch (world_x_start , world_y_start , world_z_start , world_x_end , world_y_end , world_z_end , null );
2030+ }
2031+
2032+ private AbstractSearch createSearch (final double world_x_start ,
2033+ final double world_y_start ,
2034+ final double world_z_start ,
2035+ final double world_x_end ,
2036+ final double world_y_end ,
2037+ final double world_z_end ,
2038+ final SearchSettingsSnapshot settings )
20282039 {
20292040 return createSearch (
20302041 (int ) Math .round (world_x_start / x_spacing ),
20312042 (int ) Math .round (world_y_start / y_spacing ),
20322043 (int ) Math .round (world_z_start / z_spacing ),
20332044 (int ) Math .round (world_x_end / x_spacing ),
20342045 (int ) Math .round (world_y_end / y_spacing ),
2035- (int ) Math .round (world_z_end / z_spacing ));
2046+ (int ) Math .round (world_z_end / z_spacing ),
2047+ settings );
20362048 }
20372049
20382050 /* This method uses the plugin's current search parameters to construct an isolated A* search instance using
@@ -2045,8 +2057,60 @@ private <T extends RealType<T>> AbstractSearch createSearch(final int x_start,
20452057 final int y_end ,
20462058 final int z_end )
20472059 {
2060+ return createSearch (x_start , y_start , z_start , x_end , y_end , z_end , null );
2061+ }
20482062
2049- final boolean useSecondary = isTracingOnSecondaryImageActive ();
2063+ /**
2064+ * Immutable snapshot of the search settings that {@link #createSearch} would otherwise read
2065+ * live from the enclosing {@link SNT} instance ({@link #costType}, {@link #searchImageType},
2066+ * whether tracing on the secondary/filtered image is active).
2067+ * <p>
2068+ * Interactive, click-driven tracing is meant to always use whatever is currently configured, so
2069+ * it never needs this: {@link #createSearch} falls back to the live fields when no snapshot is
2070+ * given. Batch callers that run over a long, possibly unattended period (e.g. {@code
2071+ * AStarRefiner}, invoked from {@code PathManagerUI}'s "Re-trace with A*...") should instead take
2072+ * one snapshot when the batch is queued and reuse it for every path/segment, so a setting
2073+ * changed mid-run can't silently produce a batch with inconsistent per-path settings.
2074+ *
2075+ * @see #snapshotSearchSettings()
2076+ * @see #autoTraceSync(List, PointInImage, SearchSettingsSnapshot)
2077+ */
2078+ public static final class SearchSettingsSnapshot {
2079+ final CostType costType ;
2080+ final SearchImageType searchImageType ;
2081+ final boolean useSecondary ;
2082+
2083+ private SearchSettingsSnapshot (final CostType costType , final SearchImageType searchImageType ,
2084+ final boolean useSecondary ) {
2085+ this .costType = costType ;
2086+ this .searchImageType = searchImageType ;
2087+ this .useSecondary = useSecondary ;
2088+ }
2089+ }
2090+
2091+ /**
2092+ * Captures the search settings currently configured on this instance, for use by long-running
2093+ * batch callers that need to stay internally consistent even if these settings are changed
2094+ * (via the UI) partway through the run. See {@link SearchSettingsSnapshot}.
2095+ *
2096+ * @return an immutable snapshot of the current cost function, data structure, and secondary
2097+ * (filtered) image state
2098+ */
2099+ public SearchSettingsSnapshot snapshotSearchSettings () {
2100+ return new SearchSettingsSnapshot (costType , searchImageType , isTracingOnSecondaryImageActive ());
2101+ }
2102+
2103+ private <T extends RealType <T >> AbstractSearch createSearch (final int x_start ,
2104+ final int y_start ,
2105+ final int z_start ,
2106+ final int x_end ,
2107+ final int y_end ,
2108+ final int z_end ,
2109+ final SearchSettingsSnapshot settings )
2110+ {
2111+ final boolean useSecondary = (settings != null ) ? settings .useSecondary : isTracingOnSecondaryImageActive ();
2112+ final CostType effCostType = (settings != null ) ? settings .costType : costType ;
2113+ final SearchImageType effSearchImageType = (settings != null ) ? settings .searchImageType : searchImageType ;
20502114
20512115 final RandomAccessibleInterval <T > img = useSecondary ? getSecondaryData () : getLoadedData ();
20522116
@@ -2059,11 +2123,11 @@ private <T extends RealType<T>> AbstractSearch createSearch(final int x_start,
20592123 // and those fields are shared SNT-instance state: mutating them here would race across threads!
20602124 imgStats = computeImgStats (
20612125 ImgUtils .subInterval (img , new Point (x_start , y_start , z_start ), new Point (x_end , y_end , z_end ), 10 ),
2062- new ImageStatistics (), costType );
2126+ new ImageStatistics (), effCostType );
20632127 }
20642128
20652129 Cost costFunction ;
2066- switch (costType )
2130+ switch (effCostType )
20672131 {
20682132 case RECIPROCAL :
20692133 costFunction = new Reciprocal (imgStats .min , imgStats .max );
@@ -2080,7 +2144,7 @@ private <T extends RealType<T>> AbstractSearch createSearch(final int x_start,
20802144 costFunction = new DifferenceSq (imgStats .min , imgStats .max );
20812145 break ;
20822146 default :
2083- throw new IllegalArgumentException ("BUG: Unknown cost function " + costType );
2147+ throw new IllegalArgumentException ("BUG: Unknown cost function " + effCostType );
20842148 }
20852149
20862150 Heuristic heuristic = switch (heuristicType ) {
@@ -2089,27 +2153,66 @@ private <T extends RealType<T>> AbstractSearch createSearch(final int x_start,
20892153 default -> throw new IllegalArgumentException ("BUG: Unknown heuristic " + heuristicType );
20902154 };
20912155
2156+ if (settings == null ) {
2157+ // Interactive tracing
2158+ return switch (searchType ) {
2159+ case ASTAR -> new TracerThread (
2160+ this ,
2161+ img ,
2162+ x_start ,
2163+ y_start ,
2164+ z_start ,
2165+ x_end ,
2166+ y_end ,
2167+ z_end ,
2168+ costFunction ,
2169+ heuristic );
2170+ case NBASTAR -> new BiSearch (
2171+ this ,
2172+ img ,
2173+ x_start ,
2174+ y_start ,
2175+ z_start ,
2176+ x_end ,
2177+ y_end ,
2178+ z_end ,
2179+ costFunction ,
2180+ heuristic );
2181+ default -> throw new IllegalArgumentException ("BUG: Unknown search class" );
2182+ };
2183+ }
2184+
2185+ // Batch caller with a frozen snapshot: use the explicit-parameter constructors so
2186+ // effSearchImageType is honored instead of whatever searchImageType currently is live.
2187+ // timeoutSeconds=0, reportEveryMilliseconds=1000 match the defaults the SNT-aware
2188+ // constructors above use internally (see AbstractSearch(SNT, RandomAccessibleInterval)).
20922189 return switch (searchType ) {
20932190 case ASTAR -> new TracerThread (
2094- this ,
20952191 img ,
2192+ getCalibration (),
20962193 x_start ,
20972194 y_start ,
20982195 z_start ,
20992196 x_end ,
21002197 y_end ,
21012198 z_end ,
2199+ 0 ,
2200+ 1000 ,
2201+ effSearchImageType ,
21022202 costFunction ,
21032203 heuristic );
21042204 case NBASTAR -> new BiSearch (
2105- this ,
21062205 img ,
2206+ getCalibration (),
21072207 x_start ,
21082208 y_start ,
21092209 z_start ,
21102210 x_end ,
21112211 y_end ,
21122212 z_end ,
2213+ 0 ,
2214+ 1000 ,
2215+ effSearchImageType ,
21132216 costFunction ,
21142217 heuristic );
21152218 default -> throw new IllegalArgumentException ("BUG: Unknown search class" );
@@ -2531,14 +2634,32 @@ private Path autoTraceHeadless(final List<SNTPoint> pointList, final PointInImag
25312634 * @return the stitched path, or null if any segment failed to produce a result
25322635 */
25332636 public Path autoTraceSync (final List <SNTPoint > pointList , final PointInImage forkPoint ) {
2637+ return autoTraceSync (pointList , forkPoint , null );
2638+ }
2639+
2640+ /**
2641+ * Variant of {@link #autoTraceSync(List, PointInImage)} that uses a frozen
2642+ * {@link SearchSettingsSnapshot} instead of the live cost function/data structure/secondary-image
2643+ * settings, so every segment of every path in a long-running batch (e.g. {@code AStarRefiner})
2644+ * is traced with identical settings, immune to changes made mid-run via the (deliberately still
2645+ * enabled) A* controls.
2646+ *
2647+ * @param pointList the waypoints to connect, start to end
2648+ * @param forkPoint optional fork point of the parent Path, or null
2649+ * @param settings a snapshot from {@link #snapshotSearchSettings()}, or null to read the live
2650+ * settings (equivalent to {@link #autoTraceSync(List, PointInImage)})
2651+ * @return the stitched path, or null if any segment failed to produce a result
2652+ */
2653+ public Path autoTraceSync (final List <SNTPoint > pointList , final PointInImage forkPoint ,
2654+ final SearchSettingsSnapshot settings ) {
25342655 if (pointList == null || pointList .isEmpty ())
25352656 throw new IllegalArgumentException ("pointList cannot be null or empty" );
25362657 final Path fullPath = new Path (x_spacing , y_spacing , z_spacing , spacing_units );
25372658 for (int i = 0 ; i < pointList .size () - 1 ; i ++) {
25382659 final SNTPoint start = pointList .get (i );
25392660 final SNTPoint end = pointList .get (i + 1 );
25402661 final AbstractSearch search = createSearch (start .getX (), start .getY (), start .getZ (),
2541- end .getX (), end .getY (), end .getZ ());
2662+ end .getX (), end .getY (), end .getZ (), settings );
25422663 search .run (); // synchronous: runs on the calling (already-background) thread
25432664 final Path pathResult = search .getResult ();
25442665 if (pathResult == null ) {
0 commit comments