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(),
};
});