Continuing the split-session refactor (SlicerOpenLIFU#631, SESSION_SPLIT_DESIGN.md). The legacy Pre-Planning page in pages_legacy/preplanning_page.py is ~2100 lines and mixes two distinct workflows that share little UI state.
Why split
The legacy page has two tables and two "current target" concepts:
- The Targets table with its own selection + edit mode.
- The Virtual Fit section's algorithm-input target combo, which drives a different "current target" for VF display.
These can point at different targets simultaneously, producing an ambiguous 3D scene ("which target's VF handles am I looking at?"). The legacy code accumulated a lot of ad-hoc re-entrancy guards (_input_update_in_progress, _target_table_edit_in_progress, _vf_interaction_in_progress, _placement_in_progress) to keep the two halves from stepping on each other during cross-page cascades.
Splitting into two pages puts each workflow in a self-contained page with a single canonical "current target":
- Target Selection page — targets table + Add / Import / Edit / Remove; the loaded PlanningSession's targets are the only thing this page cares about. 3D shows the volume + all target fiducials. No transducer, no skin, no VF transforms.
- Virtual Fit page (follow-up commit) — target picker at top + VF results table + Auto-Fit / Manual / Edit / Approve / Remove. 3D shows the volume + the currently-picked target + skin mesh + transducer at the selected VF pose.
Future: a Volume Segmentation page slots in before Target Selection.
This issue's scope
Land the Target Selection page first. The Virtual Fit page is a separate follow-up.
Target Selection page
New file OpenLIFU/OpenLIFUApp/pages/target_selection_page.py.
- Widget (
OpenLIFUTargetSelectionWidget):
- Header + subject / volume / protocol context labels.
- Targets table: Color swatch | Name | ID (hidden by default) | R | A | S | Show | Jump.
- Action row: Add Target | Import… | Edit (toggle) | Remove.
- Logic (
OpenLIFUTargetSelectionLogic): direct callable API for add / import / rename / move / remove / toggle-visibility. Every mutation calls mark_session_dirty -- the openlifu Session lives in memory until Save.
- Dialog (
ImportTargetDialog): pick an existing scene fiducial or load a .mrk.json / .fcsv from disk. Single call site so stays in-file per rule 7.
Navigation
- Planning Session Overview grows an Edit Targets… button that navigates to Target Selection.
- Target Selection uses the host's footer Back-to-Home for exit -- no per-page navigation button.
- Timeline participation is deferred until we have a matching Virtual Fit page.
Cross-page cascades
The legacy code cascaded target-remove / target-move into VF-approval revoke, TT-approval revoke, and solution-delete. For now:
- Removing a target from Target Selection removes the fiducial from the scene AND removes any
virtual_fit_results entries for that target from the openlifu session dict (in memory only).
- Moving a target unsets the target's approval flag in
virtual_fit_results (in memory only).
- TT / solution cascades will land with the TT and Solution Generator pages -- for now those live only on the legacy pages, which are still in
pages_legacy/ for reference.
Docs
- New
docs/pages/target-selection.md with the standard layout / API / signal-flow / acceptance-tests shape.
docs/pages/README.md -- add entry.
docs/architecture.md -- update the page-navigation diagram: Overview → Target Selection.
SESSION_SPLIT_DESIGN.md section 5.4 -- update the Planning workflow to reflect the split (Overview → Target Selection → Virtual Fit → Solution Generator → Overview).
Non-goals
- No Virtual Fit page in this commit.
- No Volume Segmentation page in this commit (comes later).
- No timeline registration yet -- Target Selection + Virtual Fit + Solution Generator will get put on the timeline together once all three exist.
- No changes to the legacy code under
pages_legacy/; it stays for reference.
Acceptance
- Load a PlanningSession. Planning Session Overview appears. Verify it now shows an Edit Targets… action.
- Click Edit Targets… → Target Selection page opens. Targets table is populated from the loaded session.
- Click Add Target. Click in a slice view. Fiducial appears, targets table adds a row, session marked dirty.
- Click Import…. Pick an existing scene fiducial. Row appears; session dirty.
- Click Edit. Change a target's Name and R/A/S cells. Fiducial updates. Session dirty.
- Click Remove on a target. Fiducial removed. If the session had
virtual_fit_results for that target, they're cleared from the in-memory dict.
- Toggle a target's Show checkbox. Fiducial visibility flips.
- Click Jump on a target. Slice views snap to the target.
- Save via toolbar → session written to disk. Data Manager shows the updated target count.
- Discard on Exit → in-memory changes drop; disk unchanged.
Relates to SlicerOpenLIFU#631, SlicerOpenLIFU#633, SlicerOpenLIFU#636.
Continuing the split-session refactor (SlicerOpenLIFU#631,
SESSION_SPLIT_DESIGN.md). The legacy Pre-Planning page inpages_legacy/preplanning_page.pyis ~2100 lines and mixes two distinct workflows that share little UI state.Why split
The legacy page has two tables and two "current target" concepts:
These can point at different targets simultaneously, producing an ambiguous 3D scene ("which target's VF handles am I looking at?"). The legacy code accumulated a lot of ad-hoc re-entrancy guards (
_input_update_in_progress,_target_table_edit_in_progress,_vf_interaction_in_progress,_placement_in_progress) to keep the two halves from stepping on each other during cross-page cascades.Splitting into two pages puts each workflow in a self-contained page with a single canonical "current target":
Future: a Volume Segmentation page slots in before Target Selection.
This issue's scope
Land the Target Selection page first. The Virtual Fit page is a separate follow-up.
Target Selection page
New file
OpenLIFU/OpenLIFUApp/pages/target_selection_page.py.OpenLIFUTargetSelectionWidget):OpenLIFUTargetSelectionLogic): direct callable API for add / import / rename / move / remove / toggle-visibility. Every mutation callsmark_session_dirty-- the openlifu Session lives in memory until Save.ImportTargetDialog): pick an existing scene fiducial or load a.mrk.json/.fcsvfrom disk. Single call site so stays in-file per rule 7.Navigation
Cross-page cascades
The legacy code cascaded target-remove / target-move into VF-approval revoke, TT-approval revoke, and solution-delete. For now:
virtual_fit_resultsentries for that target from the openlifu session dict (in memory only).virtual_fit_results(in memory only).pages_legacy/for reference.Docs
docs/pages/target-selection.mdwith the standard layout / API / signal-flow / acceptance-tests shape.docs/pages/README.md-- add entry.docs/architecture.md-- update the page-navigation diagram: Overview → Target Selection.SESSION_SPLIT_DESIGN.mdsection 5.4 -- update the Planning workflow to reflect the split (Overview → Target Selection → Virtual Fit → Solution Generator → Overview).Non-goals
pages_legacy/; it stays for reference.Acceptance
virtual_fit_resultsfor that target, they're cleared from the in-memory dict.Relates to SlicerOpenLIFU#631, SlicerOpenLIFU#633, SlicerOpenLIFU#636.