diff --git a/src/main/java/org/micromanager/lightsheetmanager/api/data/GeometryType.java b/src/main/java/org/micromanager/lightsheetmanager/api/data/GeometryType.java index 93e422e..388de48 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/api/data/GeometryType.java +++ b/src/main/java/org/micromanager/lightsheetmanager/api/data/GeometryType.java @@ -3,17 +3,19 @@ import java.util.Arrays; public enum GeometryType { - UNKNOWN("Unknown"), - DISPIM("diSPIM"), - ISPIM("iSPIM"), - OSPIM("oSPIM"), - MESOSPIM("mesoSPIM"), - SCAPE("SCAPE"); + UNKNOWN("Unknown", 45.0), + DISPIM("diSPIM", 45.0), + ISPIM("iSPIM", 45.0), + OSPIM("oSPIM", 60.0), + MESOSPIM("mesoSPIM", 45.0), + SCAPE("SCAPE", 50.0); private final String label_; + private final double firstViewAngle_; - GeometryType(final String label) { + GeometryType(final String label, final double firstViewAngle) { label_ = label; + firstViewAngle_ = firstViewAngle; } @Override @@ -21,6 +23,19 @@ public String toString() { return label_; } + /** + * Returns the nominal angle in degrees between the objective and the stage. + * + *
Each geometry is built to a different angle, so this is only a starting point + * for a new install. Every build is aligned by hand, and the angle a user measures + * on their own microscope is the one that belongs in the settings. + * + * @return the angle in degrees + */ + public double defaultFirstViewAngle() { + return firstViewAngle_; + } + public static GeometryType fromString(final String propertyValue) { if (propertyValue == null || propertyValue.isEmpty()) { return UNKNOWN; diff --git a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java index b6aacc9..f24a585 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java @@ -93,7 +93,7 @@ private void createUserInterface() { spnScanFirstViewAngle_ = Spinner.createDoubleSpinner( settings.stageScan().firstViewAngle(), - 1.0, 89.0, 1.0); + 1.0, 89.0, 0.1); // Scan CheckBoxes cbxScanFromCurrentPosition_ = new CheckBox("Scan from current position instead of center", @@ -133,13 +133,20 @@ private void createUserInterface() { pnlScanSettings.add(spnScanOvershootDist_, "wrap"); pnlScanSettings.add(lblScanRetraceSpeed, ""); pnlScanSettings.add(spnScanRetraceSpeed_, "wrap"); - pnlScanSettings.add(lblScanAngleFirstView, ""); - pnlScanSettings.add(spnScanFirstViewAngle_, "wrap"); + } + + // the grid slice step size is derived from the angle as well, so it stays + // available even when the stage cannot scan + pnlScanSettings.add(lblScanAngleFirstView, ""); + pnlScanSettings.add(spnScanFirstViewAngle_, "wrap"); + + if (isUsingScanSettings_) { pnlScanSettings.add(cbxScanFromCurrentPosition_, "span 2, wrap"); pnlScanSettings.add(cbxScanNegativeDirection_, "span 2, wrap"); pnlScanSettings.add(cbxReturnToStart_, "span 2, wrap"); } else { - pnlScanSettings.add(new JLabel("Stage scanning not supported by your firmware."), ""); + pnlScanSettings.add(new JLabel("Stage scanning not supported by your firmware."), + "span 2"); } // light sheet scanner settings panel @@ -183,9 +190,6 @@ private void createEventHandlers() { spnScanRetraceSpeed_.registerListener( () -> model_.acquisitions().settingsBuilder().stageScanBuilder() .retraceSpeed(spnScanRetraceSpeed_.getDouble())); - spnScanFirstViewAngle_.registerListener( - () -> model_.acquisitions().settingsBuilder().stageScanBuilder() - .firstViewAngle(spnScanFirstViewAngle_.getDouble())); cbxScanFromCurrentPosition_.registerListener( () -> model_.acquisitions().settingsBuilder().stageScanBuilder() @@ -198,6 +202,11 @@ private void createEventHandlers() { .returnToStart(cbxReturnToStart_.isSelected())); } + // registered outside the block above because the angle spinner is always shown + spnScanFirstViewAngle_.registerListener( + () -> model_.acquisitions().settingsBuilder().stageScanBuilder() + .firstViewAngle(spnScanFirstViewAngle_.getDouble())); + // ASIScanner Filter Freq if (isUsingPLogic_) { final ASIScanner scanner = model_.devices().device("IllumSlice"); diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java index 4f83970..2b398e6 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java @@ -65,10 +65,7 @@ public abstract class AcquisitionEngine implements AcquisitionManager, MMAcquist * Validates that the acquisition can actually be written to disk, before anything is acquired. *
* The images are written by {@code finish()}, i.e. only AFTER the run completes, so without this - * check an unusable save location is discovered at the very end and the data is lost. Observed - * 2026-07-27: a 41 s dual-camera run ended in "could not save the acquisition data to: - * D:\SCOPE\test\Test" because {@code D:\SCOPE\test} did not exist. The same path also silently - * drops {@code acq_settings.json} and {@code position_list.pos}, which are written up front. + * check an unusable save location is discovered at the very end and the data is lost. *
* Called from both geometry engines' {@code setup()} before any hardware is touched, so a failure * costs nothing and leaves the microscope untouched. @@ -125,8 +122,6 @@ protected boolean validateSaveLocation() { *
Cameras that disagree overrun the shared Core circular buffer and take the whole JVM with * them: {@code EXCEPTION_ACCESS_VIOLATION} inside {@code popNextImageMD}, no Java exception, no * recovery, no data. Refusing to arm is the only place this can be stopped from inside LSM. - * Observed in the field 2026-07-28 on a dual-Kinetix rig where a partly-applied ROI left one - * camera at 1200x1200 and the other at 600x600. * *
Called from both geometry engines' {@code setup()} before any hardware is touched, so a * failure costs nothing and leaves the microscope untouched. @@ -223,6 +218,10 @@ public AcquisitionEngine(final LightSheetManager model) { // default settings asb_ = ScapeAcquisitionSettings.builder(); + // seeded from the geometry because the angle has no single sensible default; + // a saved profile replaces it later when UserSettings loads + asb_.stageScanBuilder().firstViewAngle( + model.devices().adapter().geometry().defaultFirstViewAngle()); acqSettings_ = asb_.build(); }