diff --git a/PanTS-Demo/src/helpers/CornerstoneNifti2.tsx b/PanTS-Demo/src/helpers/CornerstoneNifti2.tsx index bc75314b..c7cfe988 100644 --- a/PanTS-Demo/src/helpers/CornerstoneNifti2.tsx +++ b/PanTS-Demo/src/helpers/CornerstoneNifti2.tsx @@ -1740,7 +1740,7 @@ export function stopCine() { } // --------------------------------------------------------------------------- -// Per-pane flip / rotate — like Cine, these act on whichever pane is currently +// Per-pane flip — like Cine, these act on whichever pane is currently // "in focus" (VisualizationPage tracks that via scroll/click and passes it in). // --------------------------------------------------------------------------- @@ -1758,23 +1758,15 @@ export function flipPaneHorizontal(pane: CinePane): void { } } -// Rotates 90° clockwise from wherever the pane currently sits (cumulative — four -// clicks return to the start). NOTE: cornerstone's rotation-angle sign convention -// relative to "on-screen clockwise" isn't independently confirmed here — if a -// case turns out to visibly rotate counter-clockwise instead, flip the `+ 90` -// below to `- 90` (mod still needs the `+ 360` to stay positive in that case). -export function rotatePane90Clockwise(pane: CinePane): void { +// Mirrors the pane top-bottom. Same toggle behavior as flipPaneHorizontal: a second +// call on the same pane un-flips it, and flip() renders by itself. +export function flipPaneVertical(pane: CinePane): void { const viewport = _getMprViewport(pane); if (!viewport) return; try { - const next = (viewport.getRotation() + 90) % 360; - viewport.setRotation(next); - // Unlike flip(), setRotation() only triggers a CAMERA_MODIFIED event — it - // never calls render() itself, so without this the rotation wouldn't show - // until some unrelated interaction happened to re-render the pane. - viewport.render(); + viewport.flip({ flipVertical: true }); } catch (e) { - console.warn(`Rotate failed for pane "${pane}":`, e); + console.warn(`Vertical flip failed for pane "${pane}":`, e); } } diff --git a/PanTS-Demo/src/helpers/compareViewer.ts b/PanTS-Demo/src/helpers/compareViewer.ts index 71b50c4a..6d898275 100644 --- a/PanTS-Demo/src/helpers/compareViewer.ts +++ b/PanTS-Demo/src/helpers/compareViewer.ts @@ -154,14 +154,14 @@ export type CompareHandle = { * queued, or the cells moved since the last pass), so the caller refits again. */ refit: (keepView?: boolean) => boolean; resetView: () => void; - // Which viewport cine/flip/rotate act on — whichever pane was last clicked/scrolled. + // Which viewport cine/flip act on — whichever pane was last clicked/scrolled. setFocusedViewport: (viewportId: string) => void; // Dotted line in each case's other 2 panes for whichever of that case's panes was // last focused — tracked independently per case, so both cases can show reference - // lines at once even though "focus" for cine/flip/rotate is a single global value. + // lines at once even though "focus" for cine/flip is a single global value. setReferenceLines: (enabled: boolean) => void; flipFocused: () => void; - rotateFocused90: () => void; + flipFocusedVertical: () => void; startCine: (fps?: number) => boolean; stopCine: () => void; // Hands the primary mouse button (on BOTH cases at once) to a measurement tool or @@ -1232,7 +1232,7 @@ async function buildCompare(els: CompareElements, src: CompareSources, gen: numb eventTarget.addEventListener(tools.Enums.Events.ANNOTATION_REMOVED, annotationCases.onRemoved); const caseKeyForAnnotation = annotationCases.caseKeyFor; - // --- Focus tracking: which viewport cine/flip/rotate act on (whichever pane was last + // --- Focus tracking: which viewport cine/flip act on (whichever pane was last // clicked/scrolled), and which pane is each case's reference-line "source" (tracked // independently per case so both cases can show reference lines simultaneously). --- let focusedViewportId: string = A.ax; @@ -1555,16 +1555,12 @@ async function buildCompare(els: CompareElements, src: CompareSources, gen: numb console.warn("[compare] flip failed:", e); } }, - rotateFocused90() { + flipFocusedVertical() { const vp = engine.getViewport(focusedViewportId) as unknown as MprViewportLike | undefined; - if (!vp) return; try { - const next = (vp.getRotation() + 90) % 360; - vp.setRotation(next); - // setRotation only triggers CAMERA_MODIFIED — it never calls render() itself. - vp.render(); + vp?.flip({ flipVertical: true }); } catch (e) { - console.warn("[compare] rotate failed:", e); + console.warn("[compare] vertical flip failed:", e); } }, startCine(fps) { diff --git a/PanTS-Demo/src/routes/CompareViewerPage.tsx b/PanTS-Demo/src/routes/CompareViewerPage.tsx index 8f08974c..0d6784ad 100644 --- a/PanTS-Demo/src/routes/CompareViewerPage.tsx +++ b/PanTS-Demo/src/routes/CompareViewerPage.tsx @@ -19,6 +19,7 @@ import { IconClick, IconClipboardList, IconFlipHorizontal, + IconFlipVertical, IconGrid3x3, IconHome, IconLasso, @@ -27,7 +28,6 @@ import { IconPlayerPause, IconPlayerPlay, IconPointer, - IconRotateClockwise, IconRuler2, IconScanEye, IconSettings, @@ -387,7 +387,7 @@ export default function CompareViewerPage() { }; const handlePaneHoverLeave = () => setHoverTip((t) => (t.visible ? { ...t, visible: false } : t)); - // Focus (which pane cine/flip/rotate act on) is tracked inside compareViewer.ts itself + // Focus (which pane cine/flip act on) is tracked inside compareViewer.ts itself // via its own mousedown/wheel listeners — this just forwards an explicit click there too, // so clicking (without dragging) a pane also moves focus, matching the single viewer. const handlePaneMouseDown = (viewportId: string) => () => handleRef.current?.setFocusedViewport(viewportId); @@ -520,12 +520,12 @@ export default function CompareViewerPage() { role="menuitem" title="The focused pane — last one clicked or scrolled" onClick={() => { - handleRef.current?.rotateFocused90(); + handleRef.current?.flipFocusedVertical(); viewFlyout.close(); }} > - - Rotate 90° clockwise + + Flip vertical , document.body diff --git a/PanTS-Demo/src/routes/VisualizationPage.tsx b/PanTS-Demo/src/routes/VisualizationPage.tsx index 61f48fab..ee903e95 100644 --- a/PanTS-Demo/src/routes/VisualizationPage.tsx +++ b/PanTS-Demo/src/routes/VisualizationPage.tsx @@ -18,6 +18,7 @@ import { IconClick, IconEye, IconFlipHorizontal, + IconFlipVertical, IconGrid3x3, IconHome, IconId, @@ -25,7 +26,6 @@ import { IconLasso, IconLayoutSidebarRight, IconListDetails, IconMicrophone, IconPlayerPause, IconPlayerPlay, IconPointer, IconReport, - IconRotateClockwise, IconRuler2, IconScanEye, IconShare, @@ -111,6 +111,7 @@ import { ELLIPSE_TOOL, enableVolume3D, flipPaneHorizontal, + flipPaneVertical, FREEHAND_ROI_TOOL, getCrosshairMm, getCurrentVolumeModality, @@ -134,7 +135,6 @@ import { resetMprOrientation, releasePrimaryMouseTools, ROI_TOOL, - rotatePane90Clockwise, setActiveMaskEditTool, setActiveMeasurementTool, setFillOpacity, @@ -1768,7 +1768,7 @@ function VisualizationPage({ liveRoom, soloChallenge, quizPractice }: Visualizat const windowFlyout = useToolbarFlyout(); // CT window presets (stays open — a config panel) const adjustFlyout = useToolbarFlyout(); // fill/border/brightness/contrast/zoom sliders + center/reset (stays open) const measureFlyout = useToolbarFlyout(); // measurement tools + magnify loupe - const viewFlyout = useToolbarFlyout(); // hover-identify, reference lines, flip, rotate + const viewFlyout = useToolbarFlyout(); // hover-identify, reference lines, flip const cineFlyout = useToolbarFlyout(); // play/pause + FPS (stays open — a live mini-panel, not a pick-and-dismiss menu) const captureFlyout = useToolbarFlyout(); // snapshot, reading session, share link const panelsFlyout = useToolbarFlyout(); // organs, organ stats, case metadata, measurements list @@ -2711,10 +2711,10 @@ function VisualizationPage({ liveRoom, soloChallenge, quizPractice }: Visualizat sessionRef.current?.log("view", `Flipped ${pane} horizontally`); }; - const handleRotate90Clockwise = () => { + const handleFlipVertical = () => { const pane = focusedPane.getFocusedPane() - rotatePane90Clockwise(pane); - sessionRef.current?.log("view", `Rotated ${pane} 90° clockwise`); + flipPaneVertical(pane); + sessionRef.current?.log("view", `Flipped ${pane} vertically`); }; // Reads cinePlaying directly rather than through setState's functional-updater form — @@ -4696,7 +4696,7 @@ const aiAvailableOrgans = useMemo(() => { )} - {/* View ▾ — hover-identify + reference lines (toggles) and flip/rotate + {/* View ▾ — hover-identify + reference lines (toggles) and flip (one-shot actions on the focused pane). */}
, document.body diff --git a/PanTS-Demo/src/test/cancerverseMasks.test.tsx b/PanTS-Demo/src/test/cancerverseMasks.test.tsx index e97dfa77..180767a6 100644 --- a/PanTS-Demo/src/test/cancerverseMasks.test.tsx +++ b/PanTS-Demo/src/test/cancerverseMasks.test.tsx @@ -85,7 +85,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/compareHdCarry.test.tsx b/PanTS-Demo/src/test/compareHdCarry.test.tsx index 893146e5..db2edff7 100644 --- a/PanTS-Demo/src/test/compareHdCarry.test.tsx +++ b/PanTS-Demo/src/test/compareHdCarry.test.tsx @@ -33,7 +33,7 @@ const handle = () => ({ setLinked: vi.fn(), setSyncCursor: vi.fn(), setSegVisible: vi.fn(), setSegOpacity: vi.fn(), setOrganVisibility: vi.fn(), applyWindow: vi.fn(), applyZoom: vi.fn(), centerCursor: vi.fn(), jumpToOrgan: vi.fn(), refit: vi.fn(), resetView: vi.fn(), setFocusedViewport: vi.fn(), - setReferenceLines: vi.fn(), flipFocused: vi.fn(), rotateFocused90: vi.fn(), startCine: vi.fn(() => true), + setReferenceLines: vi.fn(), flipFocused: vi.fn(), flipFocusedVertical: vi.fn(), startCine: vi.fn(() => true), stopCine: vi.fn(), setActiveMeasurementTool: vi.fn(), clearMeasurements: vi.fn(), getMeasurementSummaries: vi.fn(() => []), renameMeasurement: vi.fn(), removeMeasurement: vi.fn(), jumpToMeasurement: vi.fn(), subscribeToMeasurementChanges: vi.fn(() => vi.fn()), destroy: vi.fn(), diff --git a/PanTS-Demo/src/test/compareViewer.smoke.test.tsx b/PanTS-Demo/src/test/compareViewer.smoke.test.tsx index 095afd70..a974bc16 100644 --- a/PanTS-Demo/src/test/compareViewer.smoke.test.tsx +++ b/PanTS-Demo/src/test/compareViewer.smoke.test.tsx @@ -43,7 +43,7 @@ beforeEach(() => { setFocusedViewport: vi.fn(), setReferenceLines: vi.fn(), flipFocused: vi.fn(), - rotateFocused90: vi.fn(), + flipFocusedVertical: vi.fn(), startCine: vi.fn(() => true), stopCine: vi.fn(), setActiveMeasurementTool: vi.fn(), diff --git a/PanTS-Demo/src/test/cvTumorGroupAfterLoad.test.tsx b/PanTS-Demo/src/test/cvTumorGroupAfterLoad.test.tsx index 4e61501c..3f3371b8 100644 --- a/PanTS-Demo/src/test/cvTumorGroupAfterLoad.test.tsx +++ b/PanTS-Demo/src/test/cvTumorGroupAfterLoad.test.tsx @@ -94,7 +94,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/hdCancellation.test.tsx b/PanTS-Demo/src/test/hdCancellation.test.tsx index 49fe8ede..d11fdcc9 100644 --- a/PanTS-Demo/src/test/hdCancellation.test.tsx +++ b/PanTS-Demo/src/test/hdCancellation.test.tsx @@ -87,7 +87,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/organStats.test.tsx b/PanTS-Demo/src/test/organStats.test.tsx index c51b5ba5..b653e2c4 100644 --- a/PanTS-Demo/src/test/organStats.test.tsx +++ b/PanTS-Demo/src/test/organStats.test.tsx @@ -81,7 +81,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/viewer.smoke.test.tsx b/PanTS-Demo/src/test/viewer.smoke.test.tsx index 18da1bee..1e91d0eb 100644 --- a/PanTS-Demo/src/test/viewer.smoke.test.tsx +++ b/PanTS-Demo/src/test/viewer.smoke.test.tsx @@ -93,7 +93,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); @@ -105,7 +105,7 @@ vi.mock("../helpers/NiiVueNifti", () => ({ })); import { downloadBytes } from "../helpers/viewer/segmentationFile"; -import { applyRemoteMeasurement, clearMeasurements, LENGTH_TOOL, renderVisualization, getSegmentationExport, saveSegmentation, saveSharedSegmentation, subscribeToSegmentationEdits } from "../helpers/CornerstoneNifti2"; +import { applyRemoteMeasurement, clearMeasurements, LENGTH_TOOL, renderVisualization, flipPaneHorizontal, flipPaneVertical, getSegmentationExport, saveSegmentation, saveSharedSegmentation, subscribeToSegmentationEdits } from "../helpers/CornerstoneNifti2"; import VisualizationPage from "../routes/VisualizationPage"; import type { EducationResult, QuizPracticeController, SoloChallengeController } from "../education/types"; import type { LiveRoomController } from "../liveRooms/types"; @@ -356,6 +356,19 @@ describe("viewer smoke test", () => { expect(screen.queryByRole("button", { name: "Save", exact: true })).not.toBeInTheDocument(); }); + it("offers flip horizontal and flip vertical in the View menu, and no rotate", async () => { + render( + } /> + ); + await waitFor(() => expect(renderVisualization).toHaveBeenCalled()); + fireEvent.click(screen.getByRole("button", { name: "View options" })); + expect(screen.getByRole("menuitem", { name: "Flip horizontal" })).toBeInTheDocument(); + expect(screen.queryByRole("menuitem", { name: /rotate/i })).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("menuitem", { name: "Flip vertical" })); + expect(flipPaneVertical).toHaveBeenCalledTimes(1); + expect(flipPaneHorizontal).not.toHaveBeenCalled(); + }); + it("opens AI, live rooms, and shortcuts directly from the viewer toolbar", async () => { render( } /> diff --git a/PanTS-Demo/src/test/viewerBlockedStorage.test.tsx b/PanTS-Demo/src/test/viewerBlockedStorage.test.tsx index 90eb4841..37211292 100644 --- a/PanTS-Demo/src/test/viewerBlockedStorage.test.tsx +++ b/PanTS-Demo/src/test/viewerBlockedStorage.test.tsx @@ -87,7 +87,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/viewerCaseSources.test.tsx b/PanTS-Demo/src/test/viewerCaseSources.test.tsx index 3717a472..b8992ad7 100644 --- a/PanTS-Demo/src/test/viewerCaseSources.test.tsx +++ b/PanTS-Demo/src/test/viewerCaseSources.test.tsx @@ -87,7 +87,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/viewerUnsavedEdits.test.tsx b/PanTS-Demo/src/test/viewerUnsavedEdits.test.tsx index 43c16c04..8c7ea8c1 100644 --- a/PanTS-Demo/src/test/viewerUnsavedEdits.test.tsx +++ b/PanTS-Demo/src/test/viewerUnsavedEdits.test.tsx @@ -91,7 +91,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; }); diff --git a/PanTS-Demo/src/test/volume3DPresetsAcrossLoads.test.tsx b/PanTS-Demo/src/test/volume3DPresetsAcrossLoads.test.tsx index c246a52c..34e0f61a 100644 --- a/PanTS-Demo/src/test/volume3DPresetsAcrossLoads.test.tsx +++ b/PanTS-Demo/src/test/volume3DPresetsAcrossLoads.test.tsx @@ -90,7 +90,7 @@ vi.mock("../helpers/CornerstoneNifti2", async (importOriginal) => { stopCine: vi.fn(), setReferenceLinesEnabled: vi.fn(), flipPaneHorizontal: vi.fn(), - rotatePane90Clockwise: vi.fn(), + flipPaneVertical: vi.fn(), resetMprOrientation: vi.fn(), }; });