feat(slice scrollbar): Integrate ViewportSliceProgressScrollbar with customizations and docs (#5960)

Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
This commit is contained in:
Joe BoccanfusoandDan Rukas authored and GitHub committed 2026-04-23 08:46:03 -04:00
1 parent 8a2fee4d8a
commit 8fc0dc16e9
46 files changed
+1730 -279

No files matched your search

@@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react';
import ViewportImageScrollbar from './ViewportImageScrollbar';
import ViewportSliceProgressScrollbar from './ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar';
import CustomizableViewportOverlay from './CustomizableViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props: withAppTypes) {
const { viewportId, element, scrollbarHeight, servicesManager } = props;
const { cornerstoneViewportService } = servicesManager.services;
const { cornerstoneViewportService, customizationService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0,
numberOfSlices: 0,
@@ -43,17 +44,31 @@ function CornerstoneOverlays(props: withAppTypes) {
}
}
const viewportScrollbarVariant = customizationService.getCustomization('viewportScrollbar.variant');
const useProgressScrollbar = viewportScrollbarVariant !== 'legacy';
return (
<div className="noselect">
<ViewportImageScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
{useProgressScrollbar ? (
<ViewportSliceProgressScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
servicesManager={servicesManager}
/>
) : (
<ViewportImageScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
)}
<CustomizableViewportOverlay
imageSliceData={imageSliceData}
@@ -367,7 +367,7 @@ function OverlayItem(props) {
title={title}
>
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-0 mr-2 shrink-0">{value}</span>
<span className="ml-0 shrink-0">{value}</span>
</div>
);
}
@@ -0,0 +1,200 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import {
SmartScrollbar,
SmartScrollbarTrack,
SmartScrollbarFill,
SmartScrollbarIndicator,
SmartScrollbarEndpoints,
} from '@ohif/ui-next';
import { getViewportImageIds } from './helpers';
import {
useLoadedSliceBytes,
useProgressScrollbarMode,
useViewedSliceBytes,
useViewportSliceSync,
} from './hooks';
import { ViewportSliceProgressScrollbarProps } from './types';
function ViewportSliceProgressScrollbar({
viewportData,
viewportId,
element,
imageSliceData,
setImageSliceData,
servicesManager,
}: ViewportSliceProgressScrollbarProps) {
const { cineService, cornerstoneViewportService, customizationService, viewedDataService } =
servicesManager.services;
const showLoadedEndpoints =
customizationService.getCustomization('viewportScrollbar.showLoadedEndpoints') !== false;
const showLoadedFill =
customizationService.getCustomization('viewportScrollbar.showLoadedFill') !== false;
const showViewedFill =
customizationService.getCustomization('viewportScrollbar.showViewedFill') !== false;
const showLoadingPattern =
customizationService.getCustomization('viewportScrollbar.showLoadingPattern') !== false;
const viewedDwellMsRaw = customizationService.getCustomization('viewportScrollbar.viewedDwellMs');
const loadedBatchIntervalMsRaw = customizationService.getCustomization(
'viewportScrollbar.loadedBatchIntervalMs'
);
const viewedDwellMs =
typeof viewedDwellMsRaw === 'number' && viewedDwellMsRaw >= 0 ? viewedDwellMsRaw : 0;
const loadedBatchIntervalMs =
typeof loadedBatchIntervalMsRaw === 'number' && loadedBatchIntervalMsRaw >= 0
? loadedBatchIntervalMsRaw
: 200;
const { numberOfSlices, imageIndex } = imageSliceData;
const imageIds = useMemo(() => getViewportImageIds(viewportData), [viewportData]);
const imageIdToIndex = useMemo(() => {
const map = new Map<string, number>();
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
if (imageId) {
map.set(imageId, i);
}
}
return map;
}, [imageIds]);
const isFullMode = useProgressScrollbarMode({
viewportData,
viewportId,
element,
cornerstoneViewportService,
});
useViewportSliceSync({
viewportData,
viewportId,
element,
cornerstoneViewportService,
setImageSliceData,
});
const {
bytes: loadedBytes,
version: loadedVersion,
isFull: isFullyLoaded,
} = useLoadedSliceBytes({
isFullMode,
numberOfSlices,
viewportData,
imageIds,
imageIdToIndex,
loadedBatchIntervalMs,
});
const { bytes: viewedBytes, version: viewedVersion } = useViewedSliceBytes({
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
imageIdToIndex,
viewedDwellMs,
viewedDataService,
});
const onScrollbarValueChange = targetImageIndex => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const { isCineEnabled } = cineService.getState();
if (isCineEnabled) {
cineService.stopClip(element, { viewportId });
cineService.setCine({ id: viewportId, frameRate: undefined, isPlaying: false });
}
csUtils.jumpToSlice(viewport.element, {
imageIndex: targetImageIndex,
debounceLoading: true,
});
};
const isLoading = isFullMode && showLoadingPattern ? !isFullyLoaded : false;
if (!numberOfSlices || numberOfSlices <= 1) {
return null;
}
return (
<div
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
padding: '8px 5px',
zIndex: 10,
}}
>
<div
style={{
position: 'relative',
height: '100%',
width: '11px',
}}
>
<SmartScrollbar
className="absolute inset-0"
value={imageIndex || 0}
total={numberOfSlices}
onValueChange={onScrollbarValueChange}
isLoading={isLoading}
enableKeyboardNavigation={false}
aria-label="Image navigation scrollbar"
indicator={
customizationService.getCustomization('viewportScrollbar.indicator') as
| Record<string, unknown>
| undefined
}
>
<SmartScrollbarTrack>
{isFullMode && showLoadedFill && (
<SmartScrollbarFill
marked={loadedBytes}
version={loadedVersion}
className="bg-neutral/25"
loadingClassName="bg-neutral/50"
/>
)}
{isFullMode && showViewedFill && (
<SmartScrollbarFill
marked={viewedBytes}
version={viewedVersion}
className="bg-primary/35"
loadingClassName="bg-primary/35"
/>
)}
</SmartScrollbarTrack>
<SmartScrollbarIndicator />
{isFullMode && showLoadedEndpoints && (
<SmartScrollbarEndpoints
marked={loadedBytes}
version={loadedVersion}
/>
)}
</SmartScrollbar>
</div>
</div>
);
}
ViewportSliceProgressScrollbar.propTypes = {
viewportData: PropTypes.object,
viewportId: PropTypes.string.isRequired,
element: PropTypes.instanceOf(Element),
imageSliceData: PropTypes.object.isRequired,
setImageSliceData: PropTypes.func.isRequired,
servicesManager: PropTypes.object.isRequired,
};
export default ViewportSliceProgressScrollbar;
@@ -0,0 +1,40 @@
import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
import { ViewportData } from './types';
export function getImageIndexFromEvent(event): number | undefined {
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
return newImageIdIndex ?? imageIdIndex;
}
export function getViewportImageIds(viewportData: ViewportData): string[] {
if (!viewportData?.data?.length) {
return [];
}
const firstData = viewportData.data[0];
const volumeImageIds = (firstData as any).volume?.imageIds as string[] | undefined;
const datumImageIds = (firstData as any).imageIds as string[] | undefined;
return volumeImageIds || datumImageIds || [];
}
export function isProgressFullMode(viewportData: ViewportData, viewport): boolean {
if (!viewportData || !viewport || viewport instanceof VolumeViewport3D) {
return false;
}
if (viewportData.viewportType === Enums.ViewportType.STACK) {
return true;
}
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
return !!viewport.isInAcquisitionPlane?.();
}
return false;
}
export function getImageIdFromCacheEvent(event): string | undefined {
const detail = event?.detail;
return detail?.imageId || detail?.image?.imageId || detail?.cachedImage?.imageId;
}
@@ -0,0 +1,353 @@
import { useEffect, useRef, useState } from 'react';
import {
cache as cornerstoneCache,
Enums,
eventTarget,
utilities,
VolumeViewport3D,
} from '@cornerstonejs/core';
import { useByteArray } from '@ohif/ui-next';
import { getImageIdFromCacheEvent, getImageIndexFromEvent, isProgressFullMode } from './helpers';
import { ImageSliceData, ViewportData } from './types';
export function useProgressScrollbarMode({
viewportData,
viewportId,
element,
cornerstoneViewportService,
}: {
viewportData: ViewportData;
viewportId: string;
element: HTMLElement;
cornerstoneViewportService: AppTypes.CornerstoneViewportService;
}) {
const [isFullMode, setIsFullMode] = useState(false);
const lastViewPlaneNormalRef = useRef<number[] | null>(null);
/**
* Tracks whether this viewport should render full progress UI (stack or acquisition-plane
* orthographic volume) versus minimal UI. We compute once on setup and recompute on each
* CAMERA_MODIFIED event so stack->MPR transitions and acquisition-plane changes are reflected
* immediately.
*/
useEffect(() => {
if (!viewportData) {
return;
}
const updateMode = () => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportImageData = viewport?.getImageData?.();
const nextViewPlaneNormal = viewport?.getCamera?.()?.viewPlaneNormal as number[] | undefined;
// Do not update the lastViewPlaneNormalRef until we have a valid viewportImageData.
// Without viewportImageData, the viewport is not fully initialized and the isAcquisitionPlane
// check will not be accurate.
if (viewportImageData && nextViewPlaneNormal) {
lastViewPlaneNormalRef.current = [...nextViewPlaneNormal];
}
const nextMode = isProgressFullMode(viewportData, viewport);
setIsFullMode(prevMode => (prevMode === nextMode ? prevMode : nextMode));
};
updateMode();
const onCameraModified = () => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const nextViewPlaneNormal = viewport?.getCamera?.()?.viewPlaneNormal as number[] | undefined;
const previousViewPlaneNormal = lastViewPlaneNormalRef.current;
// Ignore camera updates that keep the same orientation (pan/zoom/scroll).
if (nextViewPlaneNormal && previousViewPlaneNormal) {
if (utilities.isEqual(nextViewPlaneNormal, previousViewPlaneNormal)) {
return;
}
}
updateMode();
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, onCameraModified);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, onCameraModified);
};
}, [viewportData, viewportId, cornerstoneViewportService, element]);
return isFullMode;
}
export function useViewportSliceSync({
viewportData,
viewportId,
element,
cornerstoneViewportService,
setImageSliceData,
}: {
viewportData: ViewportData;
viewportId: string;
element: HTMLElement;
cornerstoneViewportService: AppTypes.CornerstoneViewportService;
setImageSliceData: (data: ImageSliceData) => void;
}) {
/**
* Keeps shared slice state in sync: first initialize from the live viewport snapshot, then
* subscribe to navigation/render events for incremental updates while users scroll.
*/
useEffect(() => {
if (!viewportData) {
return;
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport && !(viewport instanceof VolumeViewport3D)) {
try {
const currentImageIndex = viewport.getCurrentImageIdIndex();
const currentNumberOfSlices = viewport.getNumberOfSlices();
setImageSliceData({
imageIndex: currentImageIndex,
numberOfSlices: currentNumberOfSlices,
});
} catch (error) {
console.warn(error);
}
}
const { viewportType } = viewportData;
const eventId =
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
Enums.Events.IMAGE_RENDERED;
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const nextImageIndex = getImageIndexFromEvent(event);
if (nextImageIndex == null) {
return;
}
const nextNumberOfSlices = viewport.getNumberOfSlices();
setImageSliceData({
imageIndex: nextImageIndex,
numberOfSlices: nextNumberOfSlices,
});
};
element.addEventListener(eventId, updateIndex);
return () => {
element.removeEventListener(eventId, updateIndex);
};
}, [viewportData, element, viewportId, cornerstoneViewportService, setImageSliceData]);
}
export function useLoadedSliceBytes({
isFullMode,
numberOfSlices,
viewportData,
imageIds,
imageIdToIndex,
loadedBatchIntervalMs,
}: {
isFullMode: boolean;
numberOfSlices: number;
viewportData: ViewportData;
imageIds: string[];
imageIdToIndex: Map<string, number>;
loadedBatchIntervalMs: number;
}) {
const loadedState = useByteArray(numberOfSlices || 0, loadedBatchIntervalMs);
const {
resetWith: resetLoaded,
setByte: setLoadedByte,
clearByte: clearLoadedByte,
} = loadedState;
/**
* Keeps the loaded byte array in sync with Cornerstone cache: seed from cache whenever stack /
* mode / slice count changes, then subscribe so cache add/remove updates stay incremental.
* Seeding runs immediately before registering listeners in the same effect.
*/
useEffect(() => {
if (isFullMode && numberOfSlices) {
resetLoaded(bytes => {
for (let i = 0; i < bytes.length; i++) {
const imageId = imageIds[i];
if (imageId && cornerstoneCache.isLoaded(imageId)) {
bytes[i] = 1;
}
}
});
}
if (!isFullMode || !viewportData) {
return;
}
const markLoaded = event => {
const imageId = getImageIdFromCacheEvent(event);
if (!imageId) {
return;
}
const index = imageIdToIndex.get(imageId);
if (index !== undefined) {
setLoadedByte(index);
}
};
const markRemoved = event => {
const imageId = getImageIdFromCacheEvent(event);
if (!imageId) {
return;
}
const index = imageIdToIndex.get(imageId);
if (index !== undefined) {
clearLoadedByte(index);
}
};
eventTarget.addEventListener(Enums.Events.IMAGE_CACHE_IMAGE_ADDED, markLoaded);
eventTarget.addEventListener(Enums.Events.IMAGE_CACHE_IMAGE_REMOVED, markRemoved);
return () => {
eventTarget.removeEventListener(Enums.Events.IMAGE_CACHE_IMAGE_ADDED, markLoaded);
eventTarget.removeEventListener(Enums.Events.IMAGE_CACHE_IMAGE_REMOVED, markRemoved);
};
}, [
imageIds,
isFullMode,
numberOfSlices,
viewportData,
imageIdToIndex,
resetLoaded,
setLoadedByte,
clearLoadedByte,
]);
return loadedState;
}
export function useViewedSliceBytes({
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
imageIdToIndex,
viewedDwellMs,
viewedDataService,
}: {
isFullMode: boolean;
numberOfSlices: number;
imageIndex: number;
imageIds: string[];
imageIdToIndex: Map<string, number>;
viewedDwellMs: number;
viewedDataService: AppTypes.ViewedDataService;
}) {
const viewedState = useByteArray(numberOfSlices || 0);
const { resetWith: resetViewed, setByte: setViewedByte } = viewedState;
/**
* Keeps the viewed byte array in sync with the global viewed-data store: seed from the store
* whenever stack / mode / slice count changes, then subscribe so `markDataViewed` updates stay
* incremental. Seeding runs immediately before registering the listener in the same effect.
*/
useEffect(() => {
if (isFullMode && numberOfSlices) {
resetViewed(bytes => {
for (let i = 0; i < bytes.length; i++) {
const imageId = imageIds[i];
if (imageId && viewedDataService?.isDataViewed(imageId)) {
bytes[i] = 1;
}
}
});
}
if (!viewedDataService) {
return;
}
const subscription = viewedDataService.subscribeViewedDataChanges(
({
viewedDataId,
viewedDataCleared,
}: {
viewedDataId?: string;
viewedDataCleared?: boolean;
}) => {
if (!isFullMode || !numberOfSlices) {
return;
}
if (viewedDataCleared) {
resetViewed(bytes => {
bytes.fill(0);
});
return;
}
const index = imageIdToIndex.get(viewedDataId);
if (index !== undefined) {
setViewedByte(index);
}
}
);
return () => {
subscription.unsubscribe();
};
}, [
imageIds,
isFullMode,
numberOfSlices,
imageIdToIndex,
resetViewed,
setViewedByte,
viewedDataService,
]);
/**
* Marks slices as viewed in full mode. With `viewedDwellMs === 0`, marking is immediate on
* index change; otherwise a dwell timer is used and cleaned up on subsequent changes/unmount.
*/
useEffect(() => {
if (!isFullMode || !numberOfSlices) {
return;
}
const markViewed = (targetIndex: number) => {
setViewedByte(targetIndex);
const imageId = imageIds[targetIndex];
if (imageId) {
viewedDataService?.markDataViewed(imageId);
}
};
if (viewedDwellMs === 0) {
markViewed(imageIndex || 0);
return;
}
const timerId = window.setTimeout(() => {
markViewed(imageIndex || 0);
}, viewedDwellMs);
return () => {
window.clearTimeout(timerId);
};
}, [
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
setViewedByte,
viewedDwellMs,
viewedDataService,
]);
return viewedState;
}
@@ -0,0 +1 @@
export { default } from './ViewportSliceProgressScrollbar';
@@ -0,0 +1,17 @@
import { StackViewportData, VolumeViewportData } from '../../../types/CornerstoneCacheService';
export type ViewportData = StackViewportData | VolumeViewportData;
export type ImageSliceData = {
imageIndex: number;
numberOfSlices: number;
};
export type ViewportSliceProgressScrollbarProps = {
viewportData: ViewportData | null;
viewportId: string;
element: HTMLElement;
imageSliceData: ImageSliceData;
setImageSliceData: (data: ImageSliceData) => void;
servicesManager: AppTypes.ServicesManager;
};
@@ -0,0 +1,27 @@
/**
* Default customization values for viewport scrollbar behavior.
* The `progress` scrollbar variant is in full mode for stack viewports and
* acquisition-plane orthographic viewports.
* Otherwise, viewports using the `progress` scrollbar show only the indicator.
*
* - `viewportScrollbar.variant`: `'progress' | 'legacy'` (default: `'progress'`)
* - `viewportScrollbar.showLoadedEndpoints`: show loaded-range endpoint caps in full mode
* - `viewportScrollbar.showLoadedFill`: show loaded/cached fill in full mode
* - `viewportScrollbar.showViewedFill`: show viewed fill in full mode
* - `viewportScrollbar.showLoadingPattern`: show loading pattern in full mode while not fully loaded
* - `viewportScrollbar.viewedDwellMs`: delay before marking current image viewed in full mode (ms)
* - `viewportScrollbar.loadedBatchIntervalMs`: loaded-state version batch interval in full mode (ms)
* - `viewportScrollbar.indicator`: optional custom indicator config
*/
export default function getViewportScrollbarCustomization() {
return {
'viewportScrollbar.variant': 'progress',
'viewportScrollbar.showLoadedEndpoints': true,
'viewportScrollbar.showLoadedFill': true,
'viewportScrollbar.showViewedFill': true,
'viewportScrollbar.showLoadingPattern': true,
'viewportScrollbar.viewedDwellMs': 0,
'viewportScrollbar.loadedBatchIntervalMs': 200,
'viewportScrollbar.indicator': {},
};
}
@@ -11,6 +11,7 @@ import windowLevelPresetsCustomization from './customizations/windowLevelPresets
import miscCustomization from './customizations/miscCustomization';
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
import getViewportScrollbarCustomization from './customizations/viewportScrollbarCustomization';
function getCustomizationModule({ commandsManager, servicesManager, extensionManager }) {
return [
@@ -34,6 +35,7 @@ function getCustomizationModule({ commandsManager, servicesManager, extensionMan
...miscCustomization,
...captureViewportModalCustomization,
...viewportDownloadWarningCustomization,
...getViewportScrollbarCustomization(),
},
},
];
+3
View File
@@ -21,6 +21,7 @@ import SegmentationService from './services/SegmentationService';
import CornerstoneCacheService from './services/CornerstoneCacheService';
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
import ColorbarService from './services/ColorbarService';
import ViewedDataService from './services/ViewedDataService';
import * as CornerstoneExtensionTypes from './types';
import { toolNames } from './initCornerstoneTools';
@@ -180,6 +181,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
useSelectedSegmentationsForViewportStore
.getState()
.clearSelectedSegmentationsForViewportStore();
servicesManager.services.viewedDataService?.clearViewedData();
segmentationService.removeAllSegmentations();
},
@@ -196,6 +198,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
servicesManager.registerService(SegmentationService.REGISTRATION);
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
servicesManager.registerService(ColorbarService.REGISTRATION);
servicesManager.registerService(ViewedDataService.REGISTRATION);
const { syncGroupService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
@@ -0,0 +1,58 @@
import { PubSubService } from '@ohif/core';
export type ViewedDataPayload = {
viewedDataId?: string;
viewedDataCleared?: boolean;
};
class ViewedDataService extends PubSubService {
public static readonly EVENTS = {
VIEWED_DATA_CHANGED: 'event::viewedDataChanged',
};
public static REGISTRATION = {
name: 'viewedDataService',
altName: 'ViewedDataService',
create: () => {
return new ViewedDataService();
},
};
private viewedDataIds = new Set<string>();
constructor() {
super(ViewedDataService.EVENTS);
}
public markDataViewed(dataId: string): void {
if (!dataId || this.viewedDataIds.has(dataId)) {
return;
}
this.viewedDataIds.add(dataId);
this._broadcastEvent(this.EVENTS.VIEWED_DATA_CHANGED, {
viewedDataId: dataId,
});
}
public isDataViewed(dataId: string): boolean {
if (!dataId) {
return false;
}
return this.viewedDataIds.has(dataId);
}
public clearViewedData(): void {
this.viewedDataIds.clear();
this._broadcastEvent(this.EVENTS.VIEWED_DATA_CHANGED, {
viewedDataCleared: true,
});
}
public subscribeViewedDataChanges(listener: (payload: ViewedDataPayload) => void) {
return this.subscribe(this.EVENTS.VIEWED_DATA_CHANGED, listener);
}
}
export default ViewedDataService;
@@ -0,0 +1,3 @@
import ViewedDataService from './ViewedDataService';
export default ViewedDataService;
@@ -5,6 +5,7 @@ import SegmentationServiceType from '../services/SegmentationService';
import SyncGroupServiceType from '../services/SyncGroupService';
import ToolGroupServiceType from '../services/ToolGroupService';
import ColorbarServiceType from '../services/ColorbarService';
import ViewedDataServiceType from '../services/ViewedDataService';
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
@@ -23,6 +24,7 @@ declare global {
export type SyncGroupService = SyncGroupServiceType;
export type ToolGroupService = ToolGroupServiceType;
export type ColorbarService = ColorbarServiceType;
export type ViewedDataService = ViewedDataServiceType;
export interface Services {
cornerstoneViewportService?: CornerstoneViewportServiceType;
@@ -31,6 +33,7 @@ declare global {
segmentationService?: SegmentationServiceType;
cornerstoneCacheService?: CornerstoneCacheServiceType;
colorbarService?: ColorbarServiceType;
viewedDataService?: ViewedDataServiceType;
}
export namespace Segmentation {