From d24fe54d2b5598c8356afa9ce91939018940ee19 Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:19:58 -0400 Subject: [PATCH] fix(colorbar): Allow for `AdvancedRenderingControls` to be placed at the top or bottom of the viewport. (#5270) --- .../AdvancedRenderingControls.tsx | 17 +++++++---- .../ViewportColorbar/ViewportColorbar.tsx | 8 +++-- .../ViewportColorbarsContainer.tsx | 6 ++-- .../src/hooks/useViewportMousePosition.ts | 30 ++++++++++++++----- tests/SEGHydration.spec.ts | 4 +++ tests/SEGHydrationFromMPR.spec.ts | 4 +++ 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/extensions/cornerstone/src/components/AdvancedRenderingControls/AdvancedRenderingControls.tsx b/extensions/cornerstone/src/components/AdvancedRenderingControls/AdvancedRenderingControls.tsx index 7b638cc78..d58752a46 100644 --- a/extensions/cornerstone/src/components/AdvancedRenderingControls/AdvancedRenderingControls.tsx +++ b/extensions/cornerstone/src/components/AdvancedRenderingControls/AdvancedRenderingControls.tsx @@ -1,6 +1,12 @@ import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks'; import React, { useState, useEffect, useRef } from 'react'; import { useViewportRendering } from '../../hooks'; +import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService'; + +const mouseNearControlsRanges = { + [ButtonLocation.TopMiddle]: { minX: 0, minY: 0, maxX: 1, maxY: 0.1 }, + [ButtonLocation.BottomMiddle]: { minX: 0, minY: 0.9, maxX: 1, maxY: 1 }, +}; function AdvancedRenderingControls({ viewportId, @@ -24,7 +30,7 @@ function AdvancedRenderingControls({ }); const mousePosition = useViewportMousePosition(viewportId); - const [isAtBottom, setIsAtBottom] = useState(false); + const [isMouseNearControls, setIsMouseNearControls] = useState(false); const [showAllIcons, setShowAllIcons] = useState(true); const firstMountRef = useRef(true); const { hasColorbar } = useViewportRendering(viewportId); @@ -43,10 +49,11 @@ function AdvancedRenderingControls({ useEffect(() => { if (!showAllIcons && mousePosition.isInViewport) { - if (mousePosition.isInBottomPercentage(10)) { - setIsAtBottom(true); + const mouseHoverLocation = mouseNearControlsRanges[location]; + if (mousePosition.isWithinNormalizedBox(mouseHoverLocation)) { + setIsMouseNearControls(true); } else { - setIsAtBottom(false); + setIsMouseNearControls(false); } } }, [mousePosition, showAllIcons]); @@ -97,7 +104,7 @@ function AdvancedRenderingControls({ // Always show all icons on first mount for 3 seconds // After that, always show Colorbar, show others only when mouse is at bottom - const shouldBeVisible = showAllIcons || id === 'Colorbar' || isAtBottom; + const shouldBeVisible = showAllIcons || id === 'Colorbar' || isMouseNearControls; return (
+ position === 'top' || position === 'bottom'; + /** * ViewportColorbar Component * A React wrapper for the cornerstone ViewportColorbar that adds a close button @@ -143,8 +146,9 @@ const ViewportColorbar = memo(function ViewportColorbar({ display: 'flex', alignItems: 'center', pointerEvents: 'auto', - minWidth: position === 'bottom' ? width / 2.5 : '17px', - minHeight: position === 'bottom' ? '20px' : numColorbars === 1 ? height / 3 : height / 4, + minWidth: isHorizontal(position) ? width / 2.5 : '17px', + minHeight: isHorizontal(position) ? '20px' : numColorbars === 1 ? height / 3 : height / 4, + height: '1px', // sometimes flex items with min-height need a starting point for its height calculation ...positionStylesFromConfig, }} >
diff --git a/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx b/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx index 74eafd407..b4f0fc08b 100644 --- a/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx +++ b/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo, memo, useCallback } from 'react'; import { useSystem } from '@ohif/core'; import { ColorbarCustomization } from '../../types/Colorbar'; import type { ColorMapPreset } from '../../types/Colormap'; -import ViewportColorbar from './ViewportColorbar'; +import ViewportColorbar, { isHorizontal } from './ViewportColorbar'; import useViewportRendering from '../../hooks/useViewportRendering'; import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets'; type ViewportColorbarsContainerProps = { @@ -74,10 +74,8 @@ const ViewportColorbarsContainer = memo(function ViewportColorbarsContainer({ return null; } - const isBottom = position === 'bottom'; - const isSingleViewport = viewportDisplaySets.length === 1; - const showFullList = isSingleViewport || !isBottom; + const showFullList = isSingleViewport || !isHorizontal(position); const colorbarsToUse = showFullList ? colorbars diff --git a/platform/core/src/hooks/useViewportMousePosition.ts b/platform/core/src/hooks/useViewportMousePosition.ts index 60fa12c80..442cd5952 100644 --- a/platform/core/src/hooks/useViewportMousePosition.ts +++ b/platform/core/src/hooks/useViewportMousePosition.ts @@ -2,24 +2,33 @@ import { useState, useEffect } from 'react'; import { useViewportRef } from './useViewportRef'; import { useViewportSize } from './useViewportSize'; +interface NormalizedBox { + minX: number; + minY: number; + maxX: number; + maxY: number; +} + interface MousePosition { x: number; y: number; isInViewport: boolean; + relativeX: number; relativeY: number; // Position as percentage from top (0-1) - isInBottomPercentage: (percentage: number) => boolean; + isWithinNormalizedBox?: (normalizedBox: NormalizedBox) => boolean; } function useViewportMousePosition(viewportId: string): MousePosition { const viewportRef = useViewportRef(viewportId); - const { height, clientRect } = useViewportSize(viewportId); + const { width, height, clientRect } = useViewportSize(viewportId); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0, isInViewport: false, relativeY: 0, - isInBottomPercentage: (percentage: number) => false, + relativeX: 0, + isWithinNormalizedBox: (normalizedBox: NormalizedBox) => false, }); useEffect(() => { @@ -38,18 +47,25 @@ function useViewportMousePosition(viewportId: string): MousePosition { const isInViewport = x >= 0 && x <= clientRect.width && y >= 0 && y <= clientRect.height; + const relativeX = Math.max(0, Math.min(1, x / width)); const relativeY = Math.max(0, Math.min(1, y / height)); - const isInBottomPercentage = (percentage: number) => { - return relativeY >= 1 - percentage / 100; + const isWithinNormalizedBox = (normalizedBox: NormalizedBox) => { + return ( + relativeX >= normalizedBox.minX && + relativeX <= normalizedBox.maxX && + relativeY >= normalizedBox.minY && + relativeY <= normalizedBox.maxY + ); }; setMousePosition({ x, y, isInViewport, + relativeX, relativeY, - isInBottomPercentage, + isWithinNormalizedBox, }); }; @@ -58,7 +74,7 @@ function useViewportMousePosition(viewportId: string): MousePosition { return () => { document.removeEventListener('mousemove', handleMouseMove); }; - }, [viewportRef, height, clientRect]); + }, [viewportRef, height, clientRect, width]); return mousePosition; } diff --git a/tests/SEGHydration.spec.ts b/tests/SEGHydration.spec.ts index 82c18ef56..f6abea1cc 100644 --- a/tests/SEGHydration.spec.ts +++ b/tests/SEGHydration.spec.ts @@ -10,6 +10,8 @@ test.beforeEach(async ({ page }) => { test('should hydrate SEG reports correctly', async ({ page }) => { await page.getByTestId('side-panel-header-right').click(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); + + await page.waitForTimeout(5000); await checkForScreenshot(page, page, screenShotPaths.segHydration.segPreHydration); await page.evaluate(() => { @@ -32,5 +34,7 @@ test('should hydrate SEG reports correctly', async ({ page }) => { }); await page.getByTestId('yes-hydrate-btn').click(); + + await page.waitForTimeout(5000); await checkForScreenshot(page, page, screenShotPaths.segHydration.segPostHydration); }); diff --git a/tests/SEGHydrationFromMPR.spec.ts b/tests/SEGHydrationFromMPR.spec.ts index da5b19d3b..142385afc 100644 --- a/tests/SEGHydrationFromMPR.spec.ts +++ b/tests/SEGHydrationFromMPR.spec.ts @@ -13,19 +13,23 @@ test('should properly display MPR for MR', async ({ page }) => { await page.getByTestId('Layout').click(); await page.getByTestId('MPR').click(); + await page.waitForTimeout(5000); await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprBeforeSEG); await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); + await page.waitForTimeout(5000); await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprAfterSEG); await page.getByTestId('yes-hydrate-btn').click(); + await page.waitForTimeout(5000); await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprAfterSegHydrated); await page.getByTestId('Layout').click(); await page.getByTestId('Axial Primary').click(); + await page.waitForTimeout(5000); await checkForScreenshot( page, page,