fix(colorbar): Allow for AdvancedRenderingControls to be placed at the top or bottom of the viewport. (#5270)

This commit is contained in:
Joe Boccanfuso 2025-07-28 12:19:58 -04:00 committed by GitHub
parent 1cc78112e5
commit d24fe54d2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 51 additions and 18 deletions

View File

@ -1,6 +1,12 @@
import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks'; import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useViewportRendering } from '../../hooks'; 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({ function AdvancedRenderingControls({
viewportId, viewportId,
@ -24,7 +30,7 @@ function AdvancedRenderingControls({
}); });
const mousePosition = useViewportMousePosition(viewportId); const mousePosition = useViewportMousePosition(viewportId);
const [isAtBottom, setIsAtBottom] = useState(false); const [isMouseNearControls, setIsMouseNearControls] = useState(false);
const [showAllIcons, setShowAllIcons] = useState(true); const [showAllIcons, setShowAllIcons] = useState(true);
const firstMountRef = useRef(true); const firstMountRef = useRef(true);
const { hasColorbar } = useViewportRendering(viewportId); const { hasColorbar } = useViewportRendering(viewportId);
@ -43,10 +49,11 @@ function AdvancedRenderingControls({
useEffect(() => { useEffect(() => {
if (!showAllIcons && mousePosition.isInViewport) { if (!showAllIcons && mousePosition.isInViewport) {
if (mousePosition.isInBottomPercentage(10)) { const mouseHoverLocation = mouseNearControlsRanges[location];
setIsAtBottom(true); if (mousePosition.isWithinNormalizedBox(mouseHoverLocation)) {
setIsMouseNearControls(true);
} else { } else {
setIsAtBottom(false); setIsMouseNearControls(false);
} }
} }
}, [mousePosition, showAllIcons]); }, [mousePosition, showAllIcons]);
@ -97,7 +104,7 @@ function AdvancedRenderingControls({
// Always show all icons on first mount for 3 seconds // Always show all icons on first mount for 3 seconds
// After that, always show Colorbar, show others only when mouse is at bottom // 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 ( return (
<div <div

View File

@ -27,6 +27,9 @@ type ColorbarProps = {
numColorbars: number; numColorbars: number;
}; };
export const isHorizontal = (position: ColorbarPositionType): boolean =>
position === 'top' || position === 'bottom';
/** /**
* ViewportColorbar Component * ViewportColorbar Component
* A React wrapper for the cornerstone ViewportColorbar that adds a close button * A React wrapper for the cornerstone ViewportColorbar that adds a close button
@ -143,8 +146,9 @@ const ViewportColorbar = memo(function ViewportColorbar({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
pointerEvents: 'auto', pointerEvents: 'auto',
minWidth: position === 'bottom' ? width / 2.5 : '17px', minWidth: isHorizontal(position) ? width / 2.5 : '17px',
minHeight: position === 'bottom' ? '20px' : numColorbars === 1 ? height / 3 : height / 4, 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, ...positionStylesFromConfig,
}} }}
></div> ></div>

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo, memo, useCallback } from 'react';
import { useSystem } from '@ohif/core'; import { useSystem } from '@ohif/core';
import { ColorbarCustomization } from '../../types/Colorbar'; import { ColorbarCustomization } from '../../types/Colorbar';
import type { ColorMapPreset } from '../../types/Colormap'; import type { ColorMapPreset } from '../../types/Colormap';
import ViewportColorbar from './ViewportColorbar'; import ViewportColorbar, { isHorizontal } from './ViewportColorbar';
import useViewportRendering from '../../hooks/useViewportRendering'; import useViewportRendering from '../../hooks/useViewportRendering';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets'; import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
type ViewportColorbarsContainerProps = { type ViewportColorbarsContainerProps = {
@ -74,10 +74,8 @@ const ViewportColorbarsContainer = memo(function ViewportColorbarsContainer({
return null; return null;
} }
const isBottom = position === 'bottom';
const isSingleViewport = viewportDisplaySets.length === 1; const isSingleViewport = viewportDisplaySets.length === 1;
const showFullList = isSingleViewport || !isBottom; const showFullList = isSingleViewport || !isHorizontal(position);
const colorbarsToUse = showFullList const colorbarsToUse = showFullList
? colorbars ? colorbars

View File

@ -2,24 +2,33 @@ import { useState, useEffect } from 'react';
import { useViewportRef } from './useViewportRef'; import { useViewportRef } from './useViewportRef';
import { useViewportSize } from './useViewportSize'; import { useViewportSize } from './useViewportSize';
interface NormalizedBox {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
interface MousePosition { interface MousePosition {
x: number; x: number;
y: number; y: number;
isInViewport: boolean; isInViewport: boolean;
relativeX: number;
relativeY: number; // Position as percentage from top (0-1) relativeY: number; // Position as percentage from top (0-1)
isInBottomPercentage: (percentage: number) => boolean; isWithinNormalizedBox?: (normalizedBox: NormalizedBox) => boolean;
} }
function useViewportMousePosition(viewportId: string): MousePosition { function useViewportMousePosition(viewportId: string): MousePosition {
const viewportRef = useViewportRef(viewportId); const viewportRef = useViewportRef(viewportId);
const { height, clientRect } = useViewportSize(viewportId); const { width, height, clientRect } = useViewportSize(viewportId);
const [mousePosition, setMousePosition] = useState<MousePosition>({ const [mousePosition, setMousePosition] = useState<MousePosition>({
x: 0, x: 0,
y: 0, y: 0,
isInViewport: false, isInViewport: false,
relativeY: 0, relativeY: 0,
isInBottomPercentage: (percentage: number) => false, relativeX: 0,
isWithinNormalizedBox: (normalizedBox: NormalizedBox) => false,
}); });
useEffect(() => { useEffect(() => {
@ -38,18 +47,25 @@ function useViewportMousePosition(viewportId: string): MousePosition {
const isInViewport = x >= 0 && x <= clientRect.width && y >= 0 && y <= clientRect.height; 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 relativeY = Math.max(0, Math.min(1, y / height));
const isInBottomPercentage = (percentage: number) => { const isWithinNormalizedBox = (normalizedBox: NormalizedBox) => {
return relativeY >= 1 - percentage / 100; return (
relativeX >= normalizedBox.minX &&
relativeX <= normalizedBox.maxX &&
relativeY >= normalizedBox.minY &&
relativeY <= normalizedBox.maxY
);
}; };
setMousePosition({ setMousePosition({
x, x,
y, y,
isInViewport, isInViewport,
relativeX,
relativeY, relativeY,
isInBottomPercentage, isWithinNormalizedBox,
}); });
}; };
@ -58,7 +74,7 @@ function useViewportMousePosition(viewportId: string): MousePosition {
return () => { return () => {
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
}; };
}, [viewportRef, height, clientRect]); }, [viewportRef, height, clientRect, width]);
return mousePosition; return mousePosition;
} }

View File

@ -10,6 +10,8 @@ test.beforeEach(async ({ page }) => {
test('should hydrate SEG reports correctly', async ({ page }) => { test('should hydrate SEG reports correctly', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click(); await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segHydration.segPreHydration); await checkForScreenshot(page, page, screenShotPaths.segHydration.segPreHydration);
await page.evaluate(() => { await page.evaluate(() => {
@ -32,5 +34,7 @@ test('should hydrate SEG reports correctly', async ({ page }) => {
}); });
await page.getByTestId('yes-hydrate-btn').click(); await page.getByTestId('yes-hydrate-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segHydration.segPostHydration); await checkForScreenshot(page, page, screenShotPaths.segHydration.segPostHydration);
}); });

View File

@ -13,19 +13,23 @@ test('should properly display MPR for MR', async ({ page }) => {
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprBeforeSEG); await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprBeforeSEG);
await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprAfterSEG); await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprAfterSEG);
await page.getByTestId('yes-hydrate-btn').click(); await page.getByTestId('yes-hydrate-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprAfterSegHydrated); await checkForScreenshot(page, page, screenShotPaths.segHydrationFromMPR.mprAfterSegHydrated);
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('Axial Primary').click(); await page.getByTestId('Axial Primary').click();
await page.waitForTimeout(5000);
await checkForScreenshot( await checkForScreenshot(
page, page,
page, page,