Skip to content

Commit 48b4027

Browse files
committed
project subfolders masks/ and exports/ are now created only when verso tries to populate them. This matches the behavior of the app for the annotation subfolder
1 parent eb890c3 commit 48b4027

5 files changed

Lines changed: 43 additions & 13 deletions

File tree

‎.claude/data-model.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The JSON filename defaults to `project-verso.json`
1212
```
1313
my_experiment/
1414
project-verso.json # all project state, settings, metadata
15-
thumbnails/ # working-resolution OME-TIFFs + filmstrip PNGs
15+
thumbnails/ # working-resolution OME-TIFFs
1616
masks/ # slice masks as 1-bit PNGs
1717
annotations/ # point series (points.csv) + area masks, one folder each
1818
exports/ # export outputs (images with overlays, aligned stacks, etc.)
@@ -21,6 +21,23 @@ my_experiment/
2121
<image-slug>/ # only when "Separate output per slice" is on: same files per section
2222
```
2323

24+
**Subfolders are created lazily.** Project creation makes only the folder and the
25+
JSON; each subfolder appears when something first writes into it — `thumbnails/`
26+
via `ensure_working_copy` (`engine/io/image_io.py`), `masks/` via `save_mask`
27+
(`engine/preprocessing.py`), `annotations/` via `save_annotations`
28+
(`engine/io/annotation_io.py`), `exports/` via the export writers
29+
(`engine/io/export_stack.py`, `quant_export.py`, `export_images.py`). A new writer
30+
must `mkdir(parents=True, exist_ok=True)` its own parent, and every reader must
31+
tolerate the folder being absent (`.exists()` guard).
32+
33+
In practice `thumbnails/` always exists once a project is successfully created:
34+
both the New Project and Import QUINT dialogs run `generate_thumbnails`, and the
35+
`populate_metadata` pass that follows regenerates any thumbnail still missing
36+
(including when the user hit Skip) because it needs `resolution_thumbnail_wh`.
37+
Skip therefore only forgoes the progress dialog, not the work. Missing thumbnails
38+
are never fatal regardless — every consumer goes through `ensure_working_copy`,
39+
which regenerates from the original on demand.
40+
2441
Quantification CSVs are written by `engine/io/quant_export.py`; each run gets its
2542
own timestamped folder. Pooled runs write the tables at the top level; per-slice
2643
runs write one `<slugified-image-stem>/` subfolder per section instead. Aggregated

‎CLAUDE.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,14 @@ User projects are folders, not single files:
121121
```
122122
my_experiment/
123123
project-verso.json # all state, settings, metadata (DEFAULT_PROJECT_FILENAME)
124-
thumbnails/ # working-resolution OME-TIFFs + filmstrip PNGs
124+
thumbnails/ # working-resolution OME-TIFFs
125125
masks/ # slice masks (1-bit PNG)
126+
annotations/ # point series + area masks, one folder each
126127
exports/ # export outputs (warped images, etc.)
127128
```
128129

130+
Only the project folder and its JSON are created up front. **Every subfolder is created on demand by whatever writes the first file into it** — `thumbnails/` by `ensure_working_copy`, `masks/` by `save_mask`, `annotations/` by `save_annotations`, `exports/` by the export writers. Never scaffold them eagerly: a project should not carry an empty folder for a step the user has not reached. Correspondingly, every reader must guard with `.exists()`.
131+
129132
Original images are referenced by path in `section.original_path`, not copied. See [.claude/data-model.md](.claude/data-model.md) for the JSON schema.
130133

131134
## Save policy

‎src/verso/gui/controllers/export_controller.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,16 +535,26 @@ def _annotation_titles(project_dir: Path, kind: str) -> list[str]:
535535
return titles
536536

537537
def _show_export_done(self, out_dir: Path, message: str) -> None:
538-
"""Completion box with an "Open folder" shortcut to the export dir."""
538+
"""Completion box with an "Open folder" shortcut to the export dir.
539+
540+
``exports/`` is created by whichever writer produces the first file, so a
541+
run that wrote nothing (cancelled, or a quantification with no tables)
542+
leaves it absent. The button is offered only when the folder really
543+
exists, rather than opening a path that is not there.
544+
"""
539545
from PyQt6.QtCore import QUrl
540546
from PyQt6.QtGui import QDesktopServices
541547

542548
box = QMessageBox(self._window)
543549
box.setIcon(QMessageBox.Icon.Information)
544550
box.setWindowTitle("Export complete")
545551
box.setText(message)
546-
open_btn = box.addButton("Open folder", QMessageBox.ButtonRole.ActionRole)
552+
open_btn = (
553+
box.addButton("Open folder", QMessageBox.ButtonRole.ActionRole)
554+
if out_dir.exists()
555+
else None
556+
)
547557
box.addButton(QMessageBox.StandardButton.Ok)
548558
box.exec()
549-
if box.clickedButton() is open_btn:
559+
if open_btn is not None and box.clickedButton() is open_btn:
550560
QDesktopServices.openUrl(QUrl.fromLocalFile(str(out_dir)))

‎src/verso/gui/dialogs/import_quint.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,10 +611,10 @@ def _on_accept(self) -> None:
611611
if not self._confirm_aspect_ratios(original_paths):
612612
return
613613

614+
# Only the project folder itself is created here; thumbnails/, masks/ and
615+
# exports/ are each made by whatever first writes into them (thumbnails/ by
616+
# the generate_thumbnails call below), so no empty folders are left behind.
614617
folder_path.mkdir(parents=True, exist_ok=True)
615-
(folder_path / "thumbnails").mkdir(exist_ok=True)
616-
(folder_path / "masks").mkdir(exist_ok=True)
617-
(folder_path / "exports").mkdir(exist_ok=True)
618618
project_path = folder_path / f"{slug}_verso.json"
619619

620620
orientation = self._orientation_combo.currentData() or "coronal"

‎src/verso/gui/dialogs/new_project.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -546,8 +546,11 @@ def _on_accept(self) -> None:
546546
return
547547

548548
# A project is a self-contained folder named after the project, created
549-
# inside the location the user picked. The JSON and every subfolder live
550-
# inside it; the user never names the file directly.
549+
# inside the location the user picked. The JSON lives inside it and the
550+
# user never names the file directly. Subfolders (thumbnails/, masks/,
551+
# exports/, annotations/) are not scaffolded here: each is created by
552+
# whatever writes the first file into it, so a project never carries an
553+
# empty folder for a step the user has not reached.
551554
slug = _slugify_project_name(name)
552555
folder_path = Path(location) / slug
553556
if folder_path.exists() and any(folder_path.iterdir()):
@@ -560,9 +563,6 @@ def _on_accept(self) -> None:
560563
)
561564
return
562565
folder_path.mkdir(parents=True, exist_ok=True)
563-
(folder_path / "thumbnails").mkdir(exist_ok=True)
564-
(folder_path / "masks").mkdir(exist_ok=True)
565-
(folder_path / "exports").mkdir(exist_ok=True)
566566

567567
project_path = folder_path / f"{slug}_verso.json"
568568

0 commit comments

Comments
 (0)