From d765121a528cd2fb1f62d69fd1c245cec984d6ed Mon Sep 17 00:00:00 2001 From: Aditya Sanjeev Date: Thu, 12 Nov 2026 20:21:51 -0800 Subject: [PATCH] =?UTF-8?q?Replace=20"Rotate=2090=C2=B0=20clockwise"=20wit?= =?UTF-8?q?h=20"Flip=20vertical"=20in=20the=20View=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the case viewer and the compare viewer now offer Flip horizontal and Flip vertical on the focused pane. Flip vertical uses the same toggle as flip horizontal (a second click un-flips) and logs "Flipped vertically" to the reading timeline. The rotate helpers (rotatePane90Clockwise, rotateFocused90) and the rotate icon import are removed, since nothing else called them. Tests: a viewer smoke test opens the View menu, checks that both flips are listed and no rotate item remains, and that Flip vertical calls only the vertical flip. Ablation: pointing the vertical handler at the horizontal flip fails that test. The helper mocks in the other viewer tests are renamed to match. --- PanTS-Demo/src/helpers/CornerstoneNifti2.tsx | 20 ++++++------------- PanTS-Demo/src/helpers/compareViewer.ts | 18 +++++++---------- PanTS-Demo/src/routes/CompareViewerPage.tsx | 10 +++++----- PanTS-Demo/src/routes/VisualizationPage.tsx | 20 +++++++++---------- PanTS-Demo/src/test/cancerverseMasks.test.tsx | 2 +- PanTS-Demo/src/test/compareHdCarry.test.tsx | 2 +- .../src/test/compareViewer.smoke.test.tsx | 2 +- .../src/test/cvTumorGroupAfterLoad.test.tsx | 2 +- PanTS-Demo/src/test/hdCancellation.test.tsx | 2 +- PanTS-Demo/src/test/organStats.test.tsx | 2 +- PanTS-Demo/src/test/viewer.smoke.test.tsx | 17 ++++++++++++++-- .../src/test/viewerBlockedStorage.test.tsx | 2 +- .../src/test/viewerCaseSources.test.tsx | 2 +- .../src/test/viewerUnsavedEdits.test.tsx | 2 +- .../test/volume3DPresetsAcrossLoads.test.tsx | 2 +- 15 files changed, 53 insertions(+), 52 deletions(-) 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(), }; });