OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
1 parent
8981a61b8c
commit
7ce7127d7c
803 files changed
+32760
-15893
No files matched your search
@@ -1,2 +1,3 @@
|
||||
.vercel
|
||||
test-results
|
||||
store.json
|
||||
@@ -1,7 +1,7 @@
|
||||
# docker-compose
|
||||
# --------------
|
||||
# This dockerfile is used by the `docker-compose.yml` adjacent file. When
|
||||
# running `docker-compose build`, this dockerfile helps build the "webapp" image.
|
||||
# running `docker compose build`, this dockerfile helps build the "webapp" image.
|
||||
# All paths are relative to the `context`, which is the project root directory.
|
||||
#
|
||||
# docker build
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# docker-compose
|
||||
# --------------
|
||||
# This dockerfile is used by the `docker-compose.yml` adjacent file. When
|
||||
# running `docker-compose build`, this dockerfile helps build the "webapp" image.
|
||||
# running `docker compose build`, this dockerfile helps build the "webapp" image.
|
||||
# All paths are relative to the `context`, which is the project root directory.
|
||||
#
|
||||
# docker build
|
||||
|
||||
@@ -18,12 +18,18 @@ const PUBLIC_DIR = path.join(__dirname, '../public');
|
||||
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'index.html';
|
||||
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
|
||||
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
||||
|
||||
// proxy settings
|
||||
const PROXY_TARGET = process.env.PROXY_TARGET;
|
||||
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
|
||||
const PROXY_PATH_REWRITE_FROM = process.env.PROXY_PATH_REWRITE_FROM;
|
||||
const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO;
|
||||
|
||||
const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
|
||||
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
||||
const Dotenv = require('dotenv-webpack');
|
||||
const writePluginImportFile = require('./writePluginImportsFile.js');
|
||||
// const MillionLint = require('@million/lint');
|
||||
|
||||
const copyPluginFromExtensions = writePluginImportFile(SRC_DIR, DIST_DIR);
|
||||
|
||||
@@ -75,6 +81,8 @@ module.exports = (env, argv) => {
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// For debugging re-renders
|
||||
// MillionLint.webpack(),
|
||||
new Dotenv(),
|
||||
// Clean output.path
|
||||
new CleanWebpackPlugin(),
|
||||
@@ -104,21 +112,12 @@ module.exports = (env, argv) => {
|
||||
to: `${DIST_DIR}/app-config.js`,
|
||||
},
|
||||
// Copy Dicom Microscopy Viewer build files
|
||||
// This is in pluginCOnfig.json now
|
||||
// {
|
||||
// from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import',
|
||||
// to: DIST_DIR,
|
||||
// globOptions: {
|
||||
// ignore: ['**/*.min.js.map'],
|
||||
// },
|
||||
// // The dicom-microscopy-viewer is optional, so if it doeesn't get
|
||||
// // installed, it shouldn't cause issues.
|
||||
// noErrorOnMissing: true,
|
||||
// },
|
||||
// Copy dicom-image-loader build files
|
||||
{
|
||||
from: '../../../node_modules/@cornerstonejs/dicom-image-loader/dist/dynamic-import',
|
||||
from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import',
|
||||
to: DIST_DIR,
|
||||
globOptions: {
|
||||
ignore: ['**/*.min.js.map'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -179,12 +178,23 @@ module.exports = (env, argv) => {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
devMiddleware: {
|
||||
writeToDisk: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (hasProxy) {
|
||||
mergedConfig.devServer.proxy = mergedConfig.devServer.proxy || {};
|
||||
mergedConfig.devServer.proxy[PROXY_TARGET] = PROXY_DOMAIN;
|
||||
mergedConfig.devServer.proxy = {
|
||||
[PROXY_TARGET]: {
|
||||
target: PROXY_DOMAIN,
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
[`^${PROXY_PATH_REWRITE_FROM}`]: PROXY_PATH_REWRITE_TO,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isProdBuild) {
|
||||
|
||||
@@ -13,10 +13,6 @@ export default defineConfig({
|
||||
console.log(launchOptions.args); // print all current args
|
||||
|
||||
console.log('***', browser.family, browser.name, '***');
|
||||
if (browser.family === 'chromium' && browser.name !== 'electron') {
|
||||
// auto open devtools
|
||||
launchOptions.args.push('--enable-features=SharedArrayBuffer');
|
||||
}
|
||||
|
||||
// whatever you return here becomes the launchOptions
|
||||
return launchOptions;
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('OHIF HP', () => {
|
||||
});
|
||||
|
||||
it('Should display 3 up', () => {
|
||||
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 3);
|
||||
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4);
|
||||
});
|
||||
|
||||
it('Should navigate next/previous stage', () => {
|
||||
|
||||
@@ -10,11 +10,11 @@ describe('OHIF Double Click', () => {
|
||||
});
|
||||
|
||||
it('Should double click each viewport to one up and back', () => {
|
||||
const numExpectedViewports = 3;
|
||||
const numExpectedViewports = 4;
|
||||
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', numExpectedViewports);
|
||||
|
||||
for (let i = 0; i < numExpectedViewports; i += 1) {
|
||||
cy.wait(2000);
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
cy.wait(1000);
|
||||
|
||||
// For whatever reason, with Cypress tests, we have to activate the
|
||||
// viewport we are double clicking first.
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ describe('OHIF Context Menu', function () {
|
||||
// Add length measurement
|
||||
cy.addLengthMeasurement();
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click();
|
||||
cy.get('[data-cy="measurement-item"]').as('measurementItem').click();
|
||||
cy.get('[data-cy="data-row"]').as('measurementItem').click();
|
||||
|
||||
const [x1, y1] = [150, 100];
|
||||
cy.get('@viewport')
|
||||
@@ -30,6 +30,6 @@ describe('OHIF Context Menu', function () {
|
||||
|
||||
// Click "Finding" subMenu
|
||||
cy.get('[data-cy="context-menu-item"]').as('item').contains('Aortic insufficiency').click();
|
||||
cy.get('[data-cy="measurement-item"]').as('measure-item').contains('Aortic insufficiency');
|
||||
cy.get('[data-cy="data-row"]').as('measure-item').contains('Aortic insufficiency');
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -85,7 +85,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
// The exact text is slightly dependent on the viewport resolution, so leave a range
|
||||
cy.get('@viewportInfoBottomLeft').should($txt => {
|
||||
const text = $txt.text();
|
||||
expect(text).to.include('W:118').include('L:479');
|
||||
expect(text).to.include('L:479');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
//Verify the measurement exists in the table
|
||||
cy.get('@measurementsPanel').should('be.visible');
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').as('measure').its('length').should('be.at.least', 1);
|
||||
cy.get('[data-cy="data-row"]').as('measure').its('length').should('be.at.least', 1);
|
||||
});
|
||||
|
||||
/*it('checks if angle annotation can be added on viewport without causing any errors', () => {
|
||||
@@ -351,7 +351,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
|
||||
//Verify if measurement annotation was added into the measurements panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
cy.get('[data-cy="measurement-item"]')
|
||||
cy.get('[data-cy="data-row"]')
|
||||
.its('length')
|
||||
.should('be.at.least', 2);
|
||||
|
||||
|
||||
@@ -29,16 +29,17 @@ describe('OHIF Measurement Panel', function () {
|
||||
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click();
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').as('measurementItem').click();
|
||||
cy.get('[data-cy="data-row"]').as('measurementItem').click();
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').find('svg').eq(0).as('measurementItemSvg').click();
|
||||
cy.get('[data-cy="data-row"]').find('svg').eq(0).as('measurementItemSvg').click();
|
||||
|
||||
// enter Bone label
|
||||
cy.get('[data-cy="input-annotation"]').should('exist');
|
||||
cy.get('[data-cy="input-annotation"]').should('be.visible');
|
||||
cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
|
||||
// Todo: move it to the new annotation input with drop down
|
||||
// cy.get('[data-cy="input-annotation"]').should('exist');
|
||||
// cy.get('[data-cy="input-annotation"]').should('be.visible');
|
||||
// cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').as('measurementItem').should('contain.text', 'Bone');
|
||||
// cy.get('[data-cy="data-row"]').as('measurementItem').should('contain.text', 'Bone');
|
||||
});
|
||||
|
||||
it('checks if image would jump when clicked on a measurement item', function () {
|
||||
@@ -58,7 +59,7 @@ describe('OHIF Measurement Panel', function () {
|
||||
cy.get('@viewportInfoBottomRight').should('contains.text', '(14/');
|
||||
|
||||
// Click on first measurement item
|
||||
cy.get('[data-cy="measurement-item"]').eq(0).click();
|
||||
cy.get('[data-cy="data-row"]').eq(0).click();
|
||||
|
||||
cy.get('@viewportInfoBottomRight').should('contains.text', '(1/');
|
||||
cy.get('@viewportInfoBottomRight').should('not.contains.text', '(14/');
|
||||
|
||||
@@ -39,9 +39,3 @@
|
||||
no-cache,
|
||||
no-store,
|
||||
must-revalidate'''
|
||||
|
||||
# COMMENT: For sharedArrayBuffer, see https://developer.chrome.com/blog/enabling-shared-array-buffer/
|
||||
Cross-Origin-Embedder-Policy = "require-corp"
|
||||
Cross-Origin-Opener-Policy = "same-origin"
|
||||
# set CORP to cross-origin for anyone who wants to use the viewer in an iframe
|
||||
Cross-Origin-Resource-Policy = "cross-origin"
|
||||
@@ -29,8 +29,8 @@
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:no:cache": "cross-env NODE_ENV=development webpack serve --no-cache --config .webpack/webpack.pwa.js",
|
||||
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:orthanc:no:cache": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --no-cache --config .webpack/webpack.pwa.js",
|
||||
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=http://localhost:3000/pacs/dicom-web PROXY_DOMAIN=http://localhost:8042 PROXY_PATH_REWRITE_FROM=/pacs/dicom-web PROXY_PATH_REWRITE_TO=/dicom-web APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:orthanc:no:cache": "cross-env NODE_ENV=development PROXY_TARGET=http://localhost:3000/pacs/dicom-web PROXY_DOMAIN=http://localhost:8042 PROXY_PATH_REWRITE_FROM=/pacs/dicom-web PROXY_PATH_REWRITE_TO=/dicom-web APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --no-cache --config .webpack/webpack.pwa.js",
|
||||
"dev:dcm4chee": "cross-env NODE_ENV=development APP_CONFIG=config/local_dcm4chee.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:viewer": "yarn run dev",
|
||||
@@ -53,7 +53,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.86.0",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.1.13",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.9.0-beta.105",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.105",
|
||||
@@ -100,7 +100,8 @@
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-shepherd": "6.1.1",
|
||||
"shepherd.js": "13.0.3",
|
||||
"url-loader": "^4.1.1"
|
||||
"url-loader": "^4.1.1",
|
||||
"zustand": "4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/*
|
||||
# Ensure the examples are cross-origin isolated so that
|
||||
# we can use SharedArrayBuffer
|
||||
# See https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
@@ -427,14 +427,14 @@ window.config = {
|
||||
title: 'Jumping to Measurements in the Panel',
|
||||
text: 'Click the measurement in the measurement panel to jump to it.',
|
||||
attachTo: {
|
||||
element: '[data-cy="measurement-item"]',
|
||||
element: '[data-cy="data-row"]',
|
||||
on: 'left-start',
|
||||
},
|
||||
advanceOn: {
|
||||
selector: '[data-cy="measurement-item"]',
|
||||
selector: '[data-cy="data-row"]',
|
||||
event: 'click',
|
||||
},
|
||||
beforeShowPromise: () => waitForElement('[data-cy="measurement-item"]'),
|
||||
beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
|
||||
},
|
||||
{
|
||||
id: 'changeLayout',
|
||||
|
||||
@@ -17,7 +17,6 @@ window.config = {
|
||||
showCPUFallbackMessage: true,
|
||||
showLoadingIndicator: true,
|
||||
useNorm16Texture: true,
|
||||
useSharedArrayBuffer: 'AUTO',
|
||||
maxNumRequests: {
|
||||
interaction: 100,
|
||||
thumbnail: 75,
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/ohif-viewer/',
|
||||
showStudyList: true,
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
extensions: [],
|
||||
modes: [],
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
extensions: [],
|
||||
modes: [],
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
extensions: [],
|
||||
modes: [],
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/** @type {AppTypes.Config} */
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
enableGoogleCloudAdapter: false,
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/** @type {AppTypes.Config} */
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
showStudyList: true,
|
||||
extensions: [],
|
||||
modes: [],
|
||||
|
||||
@@ -3,10 +3,6 @@ window.config = {
|
||||
routerBasename: '/',
|
||||
extensions: [],
|
||||
modes: [],
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
showStudyList: true,
|
||||
maxNumberOfWebWorkers: 3,
|
||||
showLoadingIndicator: true,
|
||||
|
||||
@@ -383,14 +383,14 @@ window.config = {
|
||||
title: 'Jumping to Measurements in the Panel',
|
||||
text: 'Click the measurement in the measurement panel to jump to it.',
|
||||
attachTo: {
|
||||
element: '[data-cy="measurement-item"]',
|
||||
element: '[data-cy="data-row"]',
|
||||
on: 'left-start',
|
||||
},
|
||||
advanceOn: {
|
||||
selector: '[data-cy="measurement-item"]',
|
||||
selector: '[data-cy="data-row"]',
|
||||
event: 'click',
|
||||
},
|
||||
beforeShowPromise: () => waitForElement('[data-cy="measurement-item"]'),
|
||||
beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
|
||||
},
|
||||
{
|
||||
id: 'changeLayout',
|
||||
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import Compose from './routes/Mode/Compose';
|
||||
import {
|
||||
ExtensionManager,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
DialogProvider,
|
||||
Modal,
|
||||
ModalProvider,
|
||||
SnackbarProvider,
|
||||
ThemeWrapper,
|
||||
ViewportDialogProvider,
|
||||
ViewportGridProvider,
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
UserAuthenticationProvider,
|
||||
ToolboxProvider,
|
||||
} from '@ohif/ui';
|
||||
import { ThemeWrapper as ThemeWrapperNext } from '@ohif/ui-next';
|
||||
import {
|
||||
ThemeWrapper as ThemeWrapperNext,
|
||||
NotificationProvider,
|
||||
TooltipProvider,
|
||||
} from '@ohif/ui-next';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
import { AppConfigProvider } from '@state';
|
||||
@@ -88,17 +92,19 @@ function App({
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl2');
|
||||
|
||||
const max3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
|
||||
appConfigState.max3DTextureSize = max3DTextureSize;
|
||||
if (gl) {
|
||||
const max3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
|
||||
appConfigState.max3DTextureSize = max3DTextureSize;
|
||||
}
|
||||
|
||||
const {
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
uiNotificationService,
|
||||
uiViewportDialogService,
|
||||
viewportGridService,
|
||||
cineService,
|
||||
userAuthenticationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
@@ -112,8 +118,8 @@ function App({
|
||||
[ViewportGridProvider, { service: viewportGridService }],
|
||||
[ViewportDialogProvider, { service: uiViewportDialogService }],
|
||||
[CineProvider, { service: cineService }],
|
||||
// [NotificationProvider, { service: uiNotificationService }],
|
||||
[SnackbarProvider, { service: uiNotificationService }],
|
||||
[NotificationProvider, { service: uiNotificationService }],
|
||||
[TooltipProvider],
|
||||
[DialogProvider, { service: uiDialogService }],
|
||||
[ModalProvider, { service: uiModalService, modal: Modal }],
|
||||
[ShepherdJourneyProvider],
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
UIDialogService,
|
||||
UIViewportDialogService,
|
||||
MeasurementService,
|
||||
StateSyncService,
|
||||
DisplaySetService,
|
||||
ToolbarService,
|
||||
ViewportGridService,
|
||||
@@ -73,7 +72,6 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
UserAuthenticationService.REGISTRATION,
|
||||
PanelService.REGISTRATION,
|
||||
WorkflowStepsService.REGISTRATION,
|
||||
StateSyncService.REGISTRATION,
|
||||
[StudyPrefetcherService.REGISTRATION, appConfig.studyPrefetcher],
|
||||
]);
|
||||
|
||||
|
||||
@@ -23,13 +23,8 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
});
|
||||
const layoutHash = useRef(null);
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
cornerstoneViewportService,
|
||||
uiNotificationService,
|
||||
} = servicesManager.services;
|
||||
const { displaySetService, measurementService, hangingProtocolService, uiNotificationService } =
|
||||
servicesManager.services;
|
||||
|
||||
const generateLayoutHash = () => `${numCols}-${numRows}`;
|
||||
|
||||
@@ -129,7 +124,7 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
title: 'Drag and Drop',
|
||||
message:
|
||||
'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
|
||||
type: 'info',
|
||||
type: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from '@ohif/ui';
|
||||
function Debug() {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<div className="flex h-screen w-screen items-center justify-center ">
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<div className="bg-secondary-dark mx-auto space-y-2 rounded-lg py-8 px-8 drop-shadow-md">
|
||||
<img
|
||||
className="mx-auto block h-14"
|
||||
@@ -24,21 +24,6 @@ function Debug() {
|
||||
}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
{!window.crossOriginIsolated && (
|
||||
<div className="text-md flex-1 text-white">
|
||||
We use SharedArrayBuffer to render volume data (e.g., MPR). If you are seeing
|
||||
this message, it means that your browser has not enabled COOP/COEP. Please see
|
||||
the following link for more information:{' '}
|
||||
<a
|
||||
href="https://web.dev/coop-coep/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-active"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes, { object } from 'prop-types';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import qs from 'query-string';
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
StudyListPagination,
|
||||
StudyListFilter,
|
||||
TooltipClipboard,
|
||||
Header,
|
||||
useModal,
|
||||
AboutModal,
|
||||
UserPreferences,
|
||||
@@ -31,10 +30,12 @@ import {
|
||||
ButtonEnums,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { Header } from '@ohif/ui-next';
|
||||
|
||||
import { Types } from '@ohif/ui';
|
||||
|
||||
import i18n from '@ohif/i18n';
|
||||
import { Onboarding } from '@ohif/ui-next';
|
||||
import { Onboarding, ScrollArea } from '@ohif/ui-next';
|
||||
|
||||
const PatientInfoVisibility = Types.PatientInfoVisibility;
|
||||
|
||||
@@ -497,29 +498,32 @@ function WorkList({
|
||||
}
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
const { component: dicomUploadComponent } =
|
||||
customizationService.get('dicomUploadComponent') ?? {};
|
||||
const { component: DicomUploadComponent } =
|
||||
customizationService.getCustomization('dicomUploadComponent') || {};
|
||||
|
||||
const uploadProps =
|
||||
dicomUploadComponent && dataSource.getConfig()?.dicomUploadEnabled
|
||||
DicomUploadComponent && dataSource.getConfig()?.dicomUploadEnabled
|
||||
? {
|
||||
title: 'Upload files',
|
||||
closeButton: true,
|
||||
shouldCloseOnEsc: false,
|
||||
shouldCloseOnOverlayClick: false,
|
||||
content: dicomUploadComponent.bind(null, {
|
||||
dataSource,
|
||||
onComplete: () => {
|
||||
hide();
|
||||
onRefresh();
|
||||
},
|
||||
onStarted: () => {
|
||||
show({
|
||||
...uploadProps,
|
||||
// when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself
|
||||
closeButton: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
content: () => (
|
||||
<DicomUploadComponent
|
||||
dataSource={dataSource}
|
||||
onComplete={() => {
|
||||
hide();
|
||||
onRefresh();
|
||||
}}
|
||||
onStarted={() => {
|
||||
show({
|
||||
...uploadProps,
|
||||
// when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself
|
||||
closeButton: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -537,45 +541,51 @@ function WorkList({
|
||||
/>
|
||||
<Onboarding />
|
||||
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
|
||||
<div className="ohif-scrollbar ohif-scrollbar-stable-gutter flex grow flex-col overflow-y-auto sm:px-5">
|
||||
<StudyListFilter
|
||||
numOfStudies={pageNumber * resultsPerPage > 100 ? 101 : numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
filterValues={{ ...filterValues, ...defaultSortValues }}
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
onUploadClick={uploadProps ? () => show(uploadProps) : undefined}
|
||||
getDataSourceConfigurationComponent={
|
||||
dataSourceConfigurationComponent ? () => dataSourceConfigurationComponent() : undefined
|
||||
}
|
||||
/>
|
||||
{hasStudies ? (
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
<ScrollArea>
|
||||
<div className="flex grow flex-col">
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource.slice(offset, offsetAndTake)}
|
||||
numOfStudies={numOfStudies}
|
||||
querying={querying}
|
||||
<StudyListFilter
|
||||
numOfStudies={pageNumber * resultsPerPage > 100 ? 101 : numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
filterValues={{ ...filterValues, ...defaultSortValues }}
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
onUploadClick={uploadProps ? () => show(uploadProps) : undefined}
|
||||
getDataSourceConfigurationComponent={
|
||||
dataSourceConfigurationComponent
|
||||
? () => dataSourceConfigurationComponent()
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="grow">
|
||||
<StudyListPagination
|
||||
onChangePage={onPageNumberChange}
|
||||
onChangePerPage={onResultsPerPageChange}
|
||||
currentPage={pageNumber}
|
||||
perPage={resultsPerPage}
|
||||
</div>
|
||||
{hasStudies ? (
|
||||
<div className="flex grow flex-col">
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource.slice(offset, offsetAndTake)}
|
||||
numOfStudies={numOfStudies}
|
||||
querying={querying}
|
||||
filtersMeta={filtersMeta}
|
||||
/>
|
||||
<div className="grow">
|
||||
<StudyListPagination
|
||||
onChangePage={onPageNumberChange}
|
||||
onChangePerPage={onResultsPerPageChange}
|
||||
currentPage={pageNumber}
|
||||
perPage={resultsPerPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
{appConfig.showLoadingIndicator && isLoadingData ? (
|
||||
<LoadingIndicatorProgress className={'h-full w-full bg-black'} />
|
||||
) : (
|
||||
<EmptyStudies />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
{appConfig.showLoadingIndicator && isLoadingData ? (
|
||||
<LoadingIndicatorProgress className={'h-full w-full bg-black'} />
|
||||
) : (
|
||||
<EmptyStudies />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { ErrorBoundary } from '@ohif/ui';
|
||||
import { ErrorBoundary } from '@ohif/ui-next';
|
||||
|
||||
// Route Components
|
||||
import DataSourceWrapper from './DataSourceWrapper';
|
||||
@@ -120,10 +120,7 @@ const createRoutes = ({
|
||||
function RouteWithErrorBoundary({ route, ...rest }) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return (
|
||||
<ErrorBoundary
|
||||
context={`Route ${route.path}`}
|
||||
fallbackRoute="/"
|
||||
>
|
||||
<ErrorBoundary context={`Route ${route.path}`}>
|
||||
<route.children
|
||||
{...rest}
|
||||
{...route.props}
|
||||
|
||||
@@ -44,7 +44,7 @@ const initUserManager = (oidc, routerBasename) => {
|
||||
post_logout_redirect_uri: _makeAbsoluteIfNecessary(post_logout_redirect_uri, baseUri),
|
||||
});
|
||||
|
||||
const client = firstOpenIdClient.useAuthorizationCodeFlow ? NextClient : LegacyClient
|
||||
const client = firstOpenIdClient.response_type === 'code' ? NextClient : LegacyClient;
|
||||
|
||||
return client(openIdConnectConfiguration);
|
||||
};
|
||||
|
||||
@@ -39,16 +39,16 @@ module.exports = {
|
||||
mono: ['Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
|
||||
},
|
||||
fontSize: {
|
||||
xs: '0.65rem',
|
||||
sm: '0.75rem',
|
||||
base: '0.8125rem',
|
||||
lg: '0.9275rem',
|
||||
xl: '1.25rem',
|
||||
'2xl': '1.5rem',
|
||||
'3xl': '1.875rem',
|
||||
'4xl': '2.25rem',
|
||||
'5xl': '3rem',
|
||||
'6xl': '4rem',
|
||||
xxs: '0.6875rem', // 11px
|
||||
xs: '0.75rem', // 12px
|
||||
sm: '0.8125rem', // 13px
|
||||
base: '0.875rem', // 14px
|
||||
lg: '1rem', // 16px
|
||||
xl: '1.125rem', // 18px
|
||||
'2xl': '1.25rem', // 20px
|
||||
'3xl': '1.375rem', // 22px
|
||||
'4xl': '1.5rem', // 24px
|
||||
'5xl': '1.875rem', // 30px
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2,3 +2,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.86.0",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.1.13",
|
||||
"@ohif/ui": "3.9.0-beta.105",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@@ -549,7 +549,7 @@ const WADO_IMAGE_LOADER = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: toNumber(instance.Rows),
|
||||
columns: toNumber(instance.Columns),
|
||||
imageOrientationPatient: toNumber(ImageOrientationPatient),
|
||||
imageOrientationPatient: toNumber(ImageOrientationPatient) || [0, 1, 0, 0, 0, -1],
|
||||
rowCosines: toNumber(rowCosines || [0, 1, 0]),
|
||||
isDefaultValueSetForRowCosine: toNumber(rowCosines) ? false : true,
|
||||
columnCosines: toNumber(columnCosines || [0, 0, -1]),
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface Extension {
|
||||
getToolbarModule?: (p: ExtensionParams) => unknown;
|
||||
getPanelModule?: (p: ExtensionParams) => unknown;
|
||||
onModeEnter?: (p: AppTypes) => void;
|
||||
onModeExit?: () => void;
|
||||
onModeExit?: (p: AppTypes) => void;
|
||||
}
|
||||
|
||||
export type ExtensionRegister = {
|
||||
@@ -440,7 +440,7 @@ export default class ExtensionManager extends PubSubService {
|
||||
|
||||
return extensionModule;
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
console.error(ex);
|
||||
throw new Error(
|
||||
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
|
||||
);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { DisplaySet } from '../types';
|
||||
|
||||
/**
|
||||
* Hook that listens for changes in the active viewport and its display sets.
|
||||
* It returns the display sets associated with the active viewport.
|
||||
*
|
||||
* @param servicesManager - Services manager instance
|
||||
* @returns Array of display sets for the active viewport
|
||||
*/
|
||||
const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
|
||||
const [displaySets, setDisplaySets] = useState<DisplaySet[]>([]);
|
||||
const { displaySetService, viewportGridService } = servicesManager.services;
|
||||
|
||||
// Move this function outside useEffect and memoize it
|
||||
const getDisplaySetsForViewport = useCallback(
|
||||
(viewportId: string) => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId) || [];
|
||||
return displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid)).filter(Boolean);
|
||||
},
|
||||
[displaySetService, viewportGridService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Get initial state
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
setDisplaySets(getDisplaySetsForViewport(viewportId));
|
||||
|
||||
const handleViewportChange = ({ viewportId }) => {
|
||||
setDisplaySets(getDisplaySetsForViewport(viewportId));
|
||||
};
|
||||
|
||||
const handleGridStateChange = ({ state }) => {
|
||||
const activeViewportId = state.activeViewportId;
|
||||
if (activeViewportId) {
|
||||
setDisplaySets(getDisplaySetsForViewport(activeViewportId));
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to viewport changes
|
||||
const subscriptions = [
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
handleViewportChange
|
||||
),
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.GRID_STATE_CHANGED,
|
||||
handleGridStateChange
|
||||
),
|
||||
];
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
};
|
||||
}, [viewportGridService, getDisplaySetsForViewport]); // Only depend on stable references
|
||||
|
||||
return displaySets;
|
||||
};
|
||||
|
||||
export default useActiveViewportDisplaySets;
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as OHIF from './index';
|
||||
|
||||
describe('Top level exports', () => {
|
||||
test('have not changed', () => {
|
||||
const expectedExports = [
|
||||
'MODULE_TYPES',
|
||||
//
|
||||
'CommandsManager',
|
||||
'ExtensionManager',
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
'ServiceProvidersManager',
|
||||
//
|
||||
'defaults',
|
||||
'utils',
|
||||
'hotkeys',
|
||||
'classes',
|
||||
'default', //
|
||||
'errorHandler',
|
||||
'string',
|
||||
'user',
|
||||
'object',
|
||||
'log',
|
||||
'DICOMWeb',
|
||||
'OHIF',
|
||||
//
|
||||
'CineService',
|
||||
'CustomizationService',
|
||||
'Enums',
|
||||
'StateSyncService',
|
||||
'UIDialogService',
|
||||
'UIModalService',
|
||||
'UINotificationService',
|
||||
'UIViewportDialogService',
|
||||
'DisplaySetService',
|
||||
'MeasurementService',
|
||||
'ToolbarService',
|
||||
'Types',
|
||||
'ViewportGridService',
|
||||
'HangingProtocolService',
|
||||
'UserAuthenticationService',
|
||||
'IWebApiDataSource',
|
||||
'DicomMetadataStore',
|
||||
'DisplaySetMessage',
|
||||
'DisplaySetMessageList',
|
||||
'pubSubServiceInterface',
|
||||
'PubSubService',
|
||||
'PanelService',
|
||||
'WorkflowStepsService',
|
||||
'StudyPrefetcherService',
|
||||
'useToolbar',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(OHIF).sort();
|
||||
|
||||
expect(exports).toEqual(expectedExports);
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
PubSubService,
|
||||
UserAuthenticationService,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
@@ -39,6 +38,7 @@ import {
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||
|
||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||
import useActiveViewportDisplaySets from './hooks/useActiveViewportDisplaySets';
|
||||
|
||||
const hotkeys = {
|
||||
...utils.hotkeys,
|
||||
@@ -68,7 +68,6 @@ const OHIF = {
|
||||
//
|
||||
CineService,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
UINotificationService,
|
||||
@@ -85,6 +84,7 @@ const OHIF = {
|
||||
PubSubService,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
useActiveViewportDisplaySets,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
};
|
||||
@@ -111,7 +111,6 @@ export {
|
||||
//
|
||||
CineService,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
UINotificationService,
|
||||
@@ -129,13 +128,15 @@ export {
|
||||
pubSubServiceInterface,
|
||||
PubSubService,
|
||||
Enums,
|
||||
Types,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
useToolbar,
|
||||
useActiveViewportDisplaySets,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
export type { Types };
|
||||
|
||||
export default OHIF;
|
||||
@@ -15,6 +15,7 @@ class CineService extends PubSubService {
|
||||
|
||||
serviceImplementation = {};
|
||||
startedClips = new Map();
|
||||
closedViewports = new Set();
|
||||
|
||||
constructor() {
|
||||
super(CineService.EVENTS);
|
||||
@@ -34,9 +35,15 @@ class CineService extends PubSubService {
|
||||
// Todo: for some reason i need to do this setTimeout since the
|
||||
// reducer state does not get updated right away and if we publish the
|
||||
// event and we use the cineService.getState() it will return the old state
|
||||
setTimeout(() => {
|
||||
if (isCineEnabled) {
|
||||
this.closedViewports.forEach(viewportId => {
|
||||
this.clearViewportCineClosed(viewportId);
|
||||
});
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isCineEnabled });
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
public playClip(element, playClipOptions) {
|
||||
@@ -68,6 +75,19 @@ class CineService extends PubSubService {
|
||||
return this.serviceImplementation._getSyncedViewports(viewportId);
|
||||
}
|
||||
|
||||
public setViewportCineClosed(viewportId) {
|
||||
this.closedViewports.add(viewportId);
|
||||
}
|
||||
|
||||
public isViewportCineClosed(viewportId) {
|
||||
// Todo: we should move towards per viewport cine closed in next release
|
||||
return this.closedViewports.size > 0;
|
||||
}
|
||||
|
||||
public clearViewportCineClosed(viewportId) {
|
||||
this.closedViewports.delete(viewportId);
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
|
||||
@@ -281,6 +281,30 @@ export default class CustomizationService extends PubSubService {
|
||||
return result.transform?.(this) || result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to easily add and retrieve customizations
|
||||
* @param id The unique identifier for the customization
|
||||
* @param defaultComponent The default component to use if no customization is set
|
||||
* @param customComponent Optional custom component to set
|
||||
* @returns The custom component if set, otherwise the default component
|
||||
*/
|
||||
public getCustomComponent(
|
||||
id: string,
|
||||
defaultComponent: React.ComponentType<any>,
|
||||
customComponent?: React.ComponentType<any>
|
||||
) {
|
||||
const customization = this.getCustomization(id, {
|
||||
id: `default-${id}`,
|
||||
content: defaultComponent,
|
||||
});
|
||||
|
||||
if (customComponent) {
|
||||
this.setModeCustomization(id, { content: customComponent });
|
||||
}
|
||||
|
||||
return customization.content;
|
||||
}
|
||||
|
||||
public addModeCustomizations(modeCustomizations): void {
|
||||
if (!modeCustomizations) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Command from '../../types/Command';
|
||||
import { Command } from '../../types/Command';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type Obj = Record<string, unknown>;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
type DisplaySet {
|
||||
displaySetInstanceUID: string;
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID?: string;
|
||||
SeriesNumber?: string;
|
||||
unsupported?: boolean;
|
||||
viewportType?: string;
|
||||
instances: any[];
|
||||
instance?: any;
|
||||
}
|
||||
|
||||
export default DisplaySet;
|
||||
@@ -204,7 +204,7 @@ export default class DisplaySetService extends PubSubService {
|
||||
}
|
||||
|
||||
// If array of instances => One instance.
|
||||
const displaySetsAdded = [];
|
||||
const displaySetsAdded = new Array<DisplaySet>();
|
||||
|
||||
if (batch) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
|
||||
@@ -50,16 +50,23 @@ const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks,
|
||||
readValues[attribute] = fromSrc[from]?.[attribute] ?? instance?.[attribute];
|
||||
}
|
||||
|
||||
// handle cases where the constraint is also a custom attribute
|
||||
const resolvedConstraint = resolveConstraintAttributes(
|
||||
readValues,
|
||||
rule.constraint,
|
||||
customAttributeRetrievalCallbacks,
|
||||
fromSrc
|
||||
);
|
||||
|
||||
// Format the constraint as required by Validate.js
|
||||
const testConstraint = {
|
||||
[attribute]: rule.constraint,
|
||||
[attribute]: resolvedConstraint,
|
||||
};
|
||||
|
||||
// Create a single attribute object to be validated, since metadataInstance is an
|
||||
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
||||
let attributeValue = readValues[attribute];
|
||||
const attributeMap = {
|
||||
[attribute]: attributeValue,
|
||||
[attribute]: readValues[attribute],
|
||||
};
|
||||
|
||||
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
||||
@@ -118,6 +125,47 @@ const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks,
|
||||
};
|
||||
};
|
||||
|
||||
// New helper function to resolve constraint attributes
|
||||
const resolveConstraintAttributes = (
|
||||
readValues,
|
||||
constraint,
|
||||
customAttributeRetrievalCallbacks,
|
||||
fromSrc
|
||||
) => {
|
||||
if (typeof constraint !== 'object' || constraint === null) {
|
||||
return constraint;
|
||||
}
|
||||
|
||||
const resolvedConstraint = {};
|
||||
Object.entries(constraint).forEach(([key, value]) => {
|
||||
if (typeof value === 'object' && Object.keys(value).length > 0 && 'attribute' in value) {
|
||||
const attributeName = value.attribute;
|
||||
const attributeFrom = value.from ?? 'metadataInstance';
|
||||
|
||||
if (customAttributeRetrievalCallbacks.hasOwnProperty(attributeName)) {
|
||||
const value = customAttributeRetrievalCallbacks[attributeName].callback.call(
|
||||
null,
|
||||
fromSrc[attributeFrom],
|
||||
fromSrc.options
|
||||
);
|
||||
|
||||
resolvedConstraint[key] = {
|
||||
value,
|
||||
};
|
||||
} else {
|
||||
resolvedConstraint[key] = {
|
||||
value:
|
||||
fromSrc[attributeFrom]?.[attributeName] ?? fromSrc.metadataInstance?.[attributeName],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
resolvedConstraint[key] = value;
|
||||
}
|
||||
}, {});
|
||||
|
||||
return resolvedConstraint;
|
||||
};
|
||||
|
||||
const HPMatcher = {
|
||||
match,
|
||||
};
|
||||
|
||||
@@ -267,7 +267,6 @@ export default class HangingProtocolService extends PubSubService {
|
||||
// and the protocol or a function that returns a protocol as the value
|
||||
const protocols = [];
|
||||
const keys = this.activeProtocolIds || this.protocols.keys();
|
||||
// @ts-ignore
|
||||
for (const protocolId of keys) {
|
||||
const protocol = this.getProtocolById(protocolId);
|
||||
if (protocol) {
|
||||
@@ -513,12 +512,6 @@ export default class HangingProtocolService extends PubSubService {
|
||||
|
||||
_validateProtocol(protocol: HangingProtocol.Protocol): HangingProtocol.Protocol {
|
||||
protocol.id = protocol.id || protocol.name;
|
||||
const defaultViewportOptions = {
|
||||
toolGroupId: 'default',
|
||||
viewportType: 'stack',
|
||||
};
|
||||
// Automatically compute some number of attributes if they
|
||||
// aren't present. Makes defining new HPs easier.
|
||||
protocol.name = protocol.name || protocol.id;
|
||||
const { stages } = protocol;
|
||||
|
||||
@@ -545,10 +538,10 @@ export default class HangingProtocolService extends PubSubService {
|
||||
const { rows, columns } = stage.viewportStructure.properties;
|
||||
|
||||
for (let i = 0; i < rows * columns; i++) {
|
||||
const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {};
|
||||
stage.viewports.push({
|
||||
viewportOptions: {
|
||||
...defaultViewportOptions,
|
||||
// Use 'default' for the first viewport, and UUIDs for the rest.
|
||||
...defaultViewport.viewportOptions,
|
||||
viewportId: i === 0 ? 'default' : uuidv4(),
|
||||
},
|
||||
displaySets: [],
|
||||
@@ -557,14 +550,14 @@ export default class HangingProtocolService extends PubSubService {
|
||||
} else {
|
||||
// Clone each viewport to ensure independent objects
|
||||
stage.viewports = stage.viewports.map((viewport, index) => {
|
||||
const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {};
|
||||
const existingViewportId = viewport.viewportOptions?.viewportId;
|
||||
|
||||
return {
|
||||
...viewport,
|
||||
viewportOptions: {
|
||||
...(viewport.viewportOptions || defaultViewportOptions),
|
||||
// use provided viewportId when available, otherwise use default for first viewport
|
||||
// and uuid for the rest
|
||||
...defaultViewport.viewportOptions,
|
||||
...viewport.viewportOptions,
|
||||
viewportId: existingViewportId
|
||||
? existingViewportId
|
||||
: index === 0
|
||||
@@ -766,6 +759,10 @@ export default class HangingProtocolService extends PubSubService {
|
||||
return viewportsToUpdate;
|
||||
}
|
||||
|
||||
public runMatchingRules(metadataArray, matchingRules, options) {
|
||||
return this.protocolEngine.findMatch(metadataArray, matchingRules, options);
|
||||
}
|
||||
|
||||
private _updateDisplaySetInstanceUIDs(
|
||||
viewport: HangingProtocol.Viewport,
|
||||
displaySetSelectorId: string,
|
||||
@@ -1111,7 +1108,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
this.stageIndex = 0;
|
||||
}
|
||||
const protocol = this.protocol;
|
||||
const stage = protocol.stages[stageIdx];
|
||||
const stage = protocol.stages[stageIdx] ?? protocol.stages[this.stageIndex];
|
||||
const defaultViewport = stage.defaultViewport || protocol.defaultViewport;
|
||||
if (!defaultViewport) {
|
||||
return;
|
||||
@@ -1407,8 +1404,8 @@ export default class HangingProtocolService extends PubSubService {
|
||||
}
|
||||
|
||||
public areRequiredSelectorsValid(
|
||||
displaySetSelectors: HangingProtocol.DisplaySetSelector,
|
||||
displaySet: any
|
||||
displaySetSelectors: HangingProtocol.DisplaySetSelector[],
|
||||
displaySet: DisplaySet
|
||||
): boolean {
|
||||
let pass = true;
|
||||
for (const displaySetSelector of displaySetSelectors) {
|
||||
@@ -1424,7 +1421,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
|
||||
private _validateRequiredSelectors(
|
||||
displaySetSelector: HangingProtocol.DisplaySetSelector,
|
||||
displaySet: any
|
||||
displaySet: DisplaySet
|
||||
) {
|
||||
const { seriesMatchingRules } = displaySetSelector;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
/* Measurement schema keys for object validation. */
|
||||
const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'uid',
|
||||
'color',
|
||||
'data',
|
||||
'getReport',
|
||||
'displayText',
|
||||
@@ -42,6 +43,8 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'frameNumber',
|
||||
'displaySetInstanceUID',
|
||||
'label',
|
||||
'isLocked',
|
||||
'isVisible',
|
||||
'description',
|
||||
'type',
|
||||
'unit',
|
||||
@@ -58,7 +61,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'shortestDiameter',
|
||||
'longestDiameter',
|
||||
'cachedStats',
|
||||
'selected',
|
||||
'isSelected',
|
||||
'textBox',
|
||||
'referencedImageId',
|
||||
];
|
||||
@@ -185,7 +188,7 @@ class MeasurementService extends PubSubService {
|
||||
return;
|
||||
}
|
||||
|
||||
measurement.selected = selected;
|
||||
measurement.isSelected = selected;
|
||||
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source: measurement.source,
|
||||
@@ -484,6 +487,10 @@ class MeasurementService extends PubSubService {
|
||||
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
||||
if (!measurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
// Todo: handle other
|
||||
@@ -555,23 +562,23 @@ class MeasurementService extends PubSubService {
|
||||
* Removes a measurement and broadcasts the removed event.
|
||||
*
|
||||
* @param {string} measurementUID The measurement uid
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
*/
|
||||
remove(measurementUID, source, eventDetails) {
|
||||
if (
|
||||
!measurementUID ||
|
||||
(!this.measurements.has(measurementUID) && !this.unmappedMeasurements.has(measurementUID))
|
||||
) {
|
||||
log.warn(`No uid provided, or unable to find measurement by uid.`);
|
||||
remove(measurementUID: string): void {
|
||||
const measurement =
|
||||
this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID);
|
||||
|
||||
if (!measurementUID || !measurement) {
|
||||
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const source = measurement.source;
|
||||
|
||||
this.unmappedMeasurements.delete(measurementUID);
|
||||
this.measurements.delete(measurementUID);
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
|
||||
source,
|
||||
measurement: measurementUID,
|
||||
...eventDetails,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -728,6 +735,57 @@ class MeasurementService extends PubSubService {
|
||||
_arrayOfObjects = obj => {
|
||||
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
|
||||
};
|
||||
|
||||
public toggleLockMeasurement(measurementUID: string): void {
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||
return;
|
||||
}
|
||||
|
||||
measurement.isLocked = !measurement.isLocked;
|
||||
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source: measurement.source,
|
||||
measurement,
|
||||
notYetUpdatedAtSource: true,
|
||||
});
|
||||
}
|
||||
|
||||
public toggleVisibilityMeasurement(measurementUID: string): void {
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||
return;
|
||||
}
|
||||
|
||||
measurement.isVisible = !measurement.isVisible;
|
||||
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source: measurement.source,
|
||||
measurement,
|
||||
notYetUpdatedAtSource: true,
|
||||
});
|
||||
}
|
||||
|
||||
public updateColorMeasurement(measurementUID: string, color: number[]): void {
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||
return;
|
||||
}
|
||||
|
||||
measurement.color = color;
|
||||
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source: measurement.source,
|
||||
measurement,
|
||||
notYetUpdatedAtSource: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default MeasurementService;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import StateSyncService from './StateSyncService';
|
||||
import log from '../../log';
|
||||
|
||||
jest.mock('../../log.js', () => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
|
||||
const extensionManager = {};
|
||||
|
||||
describe('StateSyncService.ts', () => {
|
||||
let stateSyncService;
|
||||
|
||||
let configuration;
|
||||
|
||||
beforeEach(() => {
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
configuration = {};
|
||||
stateSyncService = new StateSyncService({
|
||||
configuration,
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('init succeeds', () => {
|
||||
stateSyncService.init(extensionManager);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
|
||||
const EVENTS = {};
|
||||
|
||||
type Obj = Record<string, unknown>;
|
||||
|
||||
type StateConfig = {
|
||||
/** clearOnModeExit defines state configuration that is cleared automatically on
|
||||
* exiting a mode. This clearing occurs after the mode onModeExit,
|
||||
* so it is possible to preserve desired state during exit to be restored
|
||||
* later.
|
||||
*/
|
||||
clearOnModeExit?: boolean;
|
||||
};
|
||||
|
||||
type States = {
|
||||
[key: string]: Obj;
|
||||
};
|
||||
|
||||
/**
|
||||
*/
|
||||
export default class StateSyncService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'stateSyncService',
|
||||
create: ({ configuration = {}, commandsManager }) => {
|
||||
return new StateSyncService({ configuration, commandsManager });
|
||||
},
|
||||
};
|
||||
|
||||
extensionManager: ExtensionManager;
|
||||
configuration: Obj;
|
||||
registeredStateSets: {
|
||||
[id: string]: StateConfig;
|
||||
} = {};
|
||||
state: States = {};
|
||||
|
||||
constructor({ configuration }) {
|
||||
super(EVENTS);
|
||||
this.configuration = configuration || {};
|
||||
}
|
||||
|
||||
public init(extensionManager: ExtensionManager): void {}
|
||||
|
||||
/** Registers a new sync store called `id`. The state
|
||||
* defines how the state is stored, and any default clearing of the
|
||||
* state.
|
||||
* A default store has the lifetime of the application.
|
||||
* The other available store is cleared `onModeExit`
|
||||
*/
|
||||
public register(id: string, config: StateConfig): void {
|
||||
this.registeredStateSets[id] = config;
|
||||
this.store({ [id]: {} });
|
||||
}
|
||||
|
||||
public getState(): Record<string, Obj> {
|
||||
// TODO - return a proxy to this which is not writable in dev mode
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all the new state values contained in states.
|
||||
*
|
||||
* @param states - is an object containing replacement values to store
|
||||
* @returns
|
||||
*/
|
||||
public store(states: States): States {
|
||||
Object.keys(states).forEach(stateKey => {
|
||||
if (!this.registeredStateSets[stateKey]) {
|
||||
throw new Error(`No state ${stateKey} registered`);
|
||||
}
|
||||
});
|
||||
this.state = { ...this.state, ...states };
|
||||
return states;
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
const toReduce = {};
|
||||
for (const [key, value] of Object.entries(this.registeredStateSets)) {
|
||||
if (value.clearOnModeExit) {
|
||||
toReduce[key] = {};
|
||||
}
|
||||
}
|
||||
this.store(toReduce);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import StateSyncService from './StateSyncService';
|
||||
|
||||
export default StateSyncService;
|
||||
@@ -4,7 +4,16 @@ import ServicesManager from '../ServicesManager';
|
||||
import ViewportGridService from '../ViewportGridService';
|
||||
import { DisplaySet } from '../../types';
|
||||
|
||||
const IMAGE_REQUEST_TYPE = 'prefetch';
|
||||
enum RequestType {
|
||||
/** Highest priority for loading*/
|
||||
Interaction = 'interaction',
|
||||
/** Second highest priority for loading*/
|
||||
Thumbnail = 'thumbnail',
|
||||
/** Third highest priority for loading, usually used for image loading in the background*/
|
||||
Prefetch = 'prefetch',
|
||||
/** Lower priority, often used for background computations in the worker */
|
||||
Compute = 'compute',
|
||||
}
|
||||
|
||||
export const EVENTS = {
|
||||
SERVICE_STARTED: 'event::studyPrefetcherService:started',
|
||||
@@ -67,7 +76,7 @@ interface ICache {
|
||||
}
|
||||
|
||||
interface IImageLoadPoolManager {
|
||||
addRequest (
|
||||
addRequest(
|
||||
requestFn: () => Promise<any>,
|
||||
type: string,
|
||||
additionalDetails: Record<string, unknown>,
|
||||
@@ -81,7 +90,7 @@ interface IImageLoader {
|
||||
}
|
||||
|
||||
type EventSubscription = {
|
||||
unsubscribe: () => void
|
||||
unsubscribe: () => void;
|
||||
};
|
||||
|
||||
interface IImageLoadEventsManager {
|
||||
@@ -122,7 +131,7 @@ class StudyPrefetcherService extends PubSubService {
|
||||
};
|
||||
|
||||
// Properties set by Cornerstone extension (initStudyPrefetcherService)
|
||||
public requestType: string = IMAGE_REQUEST_TYPE;
|
||||
public requestType: string = RequestType.Prefetch;
|
||||
public cache: ICache;
|
||||
public imageLoadPoolManager: IImageLoadPoolManager;
|
||||
public imageLoader: IImageLoader;
|
||||
@@ -181,22 +190,22 @@ class StudyPrefetcherService extends PubSubService {
|
||||
if (!this._inflightRequests.get(imageId)) {
|
||||
this._sendNextRequests();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fnImageLoadedEventListener = (evt) => {
|
||||
const fnImageLoadedEventListener = evt => {
|
||||
const { image } = evt.detail;
|
||||
const { imageId } = image;
|
||||
|
||||
this._moveImageIdToLoadedSet(imageId);
|
||||
fnOnImageLoadCompleted(imageId);
|
||||
}
|
||||
};
|
||||
|
||||
const fnImageLoadFailedEventListener = (evt) => {
|
||||
const fnImageLoadFailedEventListener = evt => {
|
||||
const { imageId } = evt.detail;
|
||||
|
||||
this._moveImageIdToFailedSet(imageId);
|
||||
fnOnImageLoadCompleted(imageId);
|
||||
}
|
||||
};
|
||||
|
||||
return this.imageLoadEventsManager.addEventListeners(
|
||||
fnImageLoadedEventListener,
|
||||
@@ -246,7 +255,7 @@ class StudyPrefetcherService extends PubSubService {
|
||||
viewportGridActiveViewportIdSubscription,
|
||||
viewportGridLayoutChangedSubscription,
|
||||
viewportGridStateChangedSubscription,
|
||||
viewportGridViewportreadySubscription
|
||||
viewportGridViewportreadySubscription,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -263,15 +272,13 @@ class StudyPrefetcherService extends PubSubService {
|
||||
this._subscriptions = [];
|
||||
}
|
||||
|
||||
private _syncWithActiveViewport(
|
||||
{
|
||||
activeViewportId,
|
||||
forceRestart
|
||||
}:
|
||||
{
|
||||
activeViewportId?: string,
|
||||
forceRestart?: boolean
|
||||
} = {}) {
|
||||
private _syncWithActiveViewport({
|
||||
activeViewportId,
|
||||
forceRestart,
|
||||
}: {
|
||||
activeViewportId?: string;
|
||||
forceRestart?: boolean;
|
||||
} = {}) {
|
||||
const { viewportGridService } = this._servicesManager.services;
|
||||
const viewportGridServiceState = viewportGridService.getState();
|
||||
const { viewports } = viewportGridServiceState;
|
||||
@@ -311,8 +318,13 @@ class StudyPrefetcherService extends PubSubService {
|
||||
private _areActiveDisplaySetsLoaded() {
|
||||
const { _activeDisplaySetsInstanceUIDs: displaySetsInstanceUIDs } = this;
|
||||
|
||||
return displaySetsInstanceUIDs.length && displaySetsInstanceUIDs.every(displaySetsInstanceUID =>
|
||||
this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1);
|
||||
return (
|
||||
displaySetsInstanceUIDs.length &&
|
||||
displaySetsInstanceUIDs.every(
|
||||
displaySetsInstanceUID =>
|
||||
this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _getClosestDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
|
||||
@@ -363,7 +375,9 @@ class StudyPrefetcherService extends PubSubService {
|
||||
const { displaySetsCount } = this.config;
|
||||
const activeDisplaySetsInstanceUIDs = this._activeDisplaySetsInstanceUIDs;
|
||||
const [activeDisplaySetUID] = activeDisplaySetsInstanceUIDs;
|
||||
const activeDisplaySetIndex = displaySets.findIndex(ds => ds.displaySetInstanceUID === activeDisplaySetUID);
|
||||
const activeDisplaySetIndex = displaySets.findIndex(
|
||||
ds => ds.displaySetInstanceUID === activeDisplaySetUID
|
||||
);
|
||||
const getDisplaySetsFunctionsMap = {
|
||||
[StudyPrefetchOrder.closest]: this._getClosestDisplaySets,
|
||||
[StudyPrefetchOrder.downward]: this._getDownwardDisplaySets,
|
||||
@@ -381,23 +395,21 @@ class StudyPrefetcherService extends PubSubService {
|
||||
|
||||
// Remove any active displaySet that may still be in the activeDisplaySetsInstanceUIDs.
|
||||
// That may happen when activeDisplaySetsInstanceUIDs has more than one element.
|
||||
return fnGetDisplaySets.call(this, displaySets, activeDisplaySetIndex).filter(
|
||||
ds => !uidsSet.has(ds.displaySetInstanceUID)
|
||||
).slice(0, displaySetsCount);
|
||||
return fnGetDisplaySets
|
||||
.call(this, displaySets, activeDisplaySetIndex)
|
||||
.filter(ds => !uidsSet.has(ds.displaySetInstanceUID))
|
||||
.slice(0, displaySetsCount);
|
||||
}
|
||||
|
||||
private _getDisplaySets() {
|
||||
const { displaySetService } = this._servicesManager.services;
|
||||
const displaySets = [...displaySetService.getActiveDisplaySets()];
|
||||
let displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
|
||||
const displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
|
||||
|
||||
return { displaySets, displaySetsToPrefetch };
|
||||
}
|
||||
|
||||
private _updateImageIdsDisplaySetMap(
|
||||
displaySetInstanceUID: string,
|
||||
imageIds: string[]
|
||||
): void {
|
||||
private _updateImageIdsDisplaySetMap(displaySetInstanceUID: string, imageIds: string[]): void {
|
||||
for (const imageId of imageIds) {
|
||||
let displaySetsInstanceUIDsMap = this._imageIdsToDisplaySetsMap.get(imageId);
|
||||
|
||||
@@ -451,12 +463,12 @@ class StudyPrefetcherService extends PubSubService {
|
||||
pendingImageIds,
|
||||
loadedImageIds,
|
||||
failedImageIds: new Set(),
|
||||
loadingProgress: 0
|
||||
loadingProgress: 0,
|
||||
};
|
||||
|
||||
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
|
||||
this._displaySetLoadingStates.set(displaySetInstanceUID, displaySetLoadingState);
|
||||
this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds)
|
||||
this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds);
|
||||
|
||||
// Notify the UI that something is already loaded (eg: update StudyBrowser)
|
||||
if (loadedImageIds.size) {
|
||||
|
||||
@@ -116,8 +116,10 @@ export default class ToolbarService extends PubSubService {
|
||||
* Removes buttons from the toolbar.
|
||||
* @param buttonId - The button to be removed.
|
||||
*/
|
||||
public removeButton(buttonId : string ){
|
||||
if(this.state.buttons[buttonId]) delete this.state.buttons[buttonId]
|
||||
public removeButton(buttonId: string) {
|
||||
if (this.state.buttons[buttonId]) {
|
||||
delete this.state.buttons[buttonId];
|
||||
}
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
...this.state,
|
||||
});
|
||||
@@ -555,6 +557,11 @@ export default class ToolbarService extends PubSubService {
|
||||
};
|
||||
|
||||
getButtonComponentForUIType(uiType: string) {
|
||||
return uiType ? this._getButtonUITypes()[uiType]?.defaultComponent ?? null : null;
|
||||
return uiType ? (this._getButtonUITypes()[uiType]?.defaultComponent ?? null) : null;
|
||||
}
|
||||
|
||||
clearButtonSection(buttonSection: string) {
|
||||
this.state.buttonSections[buttonSection] = [];
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
|
||||
export type EvaluatePublic = string | EvaluateFunction | string[];
|
||||
export type EvaluatePublic =
|
||||
| string
|
||||
| EvaluateFunction
|
||||
| EvaluateObject
|
||||
| (string | EvaluateFunction | EvaluateObject)[];
|
||||
|
||||
export type EvaluateFunction = (props: Record<string, unknown>) => {
|
||||
disabled: boolean;
|
||||
className: string;
|
||||
};
|
||||
|
||||
export type EvaluateObject = {
|
||||
name: string;
|
||||
// Allow any additional properties
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ButtonProps = {
|
||||
id: string;
|
||||
icon: string;
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
/**
|
||||
* A UI Notification
|
||||
*
|
||||
* @typedef {Object} Notification
|
||||
* @property {string} title -
|
||||
* @property {string} message -
|
||||
* @property {number} [duration=5000] - in ms
|
||||
* @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
|
||||
* @property {string} [type="info"] - "info" | "error" | "warning" | "success"
|
||||
* @property {boolean} [autoClose=true]
|
||||
*/
|
||||
|
||||
const serviceShowRequestQueue = [];
|
||||
|
||||
const serviceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_hide: () => console.debug('hide() NOT IMPLEMENTED'),
|
||||
_show: showArguments => {
|
||||
serviceShowRequestQueue.push(showArguments);
|
||||
|
||||
console.warn('show() NOT IMPLEMENTED');
|
||||
console.debug('show() NOT IMPLEMENTED');
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
type ToastType = 'success' | 'error' | 'info' | 'warning' | 'loading';
|
||||
|
||||
class UINotificationService {
|
||||
static REGISTRATION = {
|
||||
name: 'uiNotificationService',
|
||||
@@ -44,11 +31,6 @@ class UINotificationService {
|
||||
}
|
||||
if (showImplementation) {
|
||||
serviceImplementation._show = showImplementation;
|
||||
|
||||
while (serviceShowRequestQueue.length > 0) {
|
||||
const showArguments = serviceShowRequestQueue.pop();
|
||||
serviceImplementation._show(showArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,24 +41,103 @@ class UINotificationService {
|
||||
* @returns undefined
|
||||
*/
|
||||
public hide(id: string) {
|
||||
return serviceImplementation._hide({ id });
|
||||
return serviceImplementation._hide(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a new UI notification; returns the
|
||||
* ID of the created notification.
|
||||
* ID of the created notification. Can also handle promises for loading states.
|
||||
*
|
||||
* @param {Notification} notification { title, message, duration, position, type, autoClose}
|
||||
* @returns {number} id
|
||||
* @param {object} notification - The notification object
|
||||
* @param {string} notification.title - The title of the notification
|
||||
* @param {string | function} notification.message - The message content of the notification or a function that returns a message
|
||||
* @param {number} [notification.duration=5000] - The duration to show the notification (in milliseconds)
|
||||
* @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'} [notification.position='bottom-right'] - The position of the notification
|
||||
* @param {ToastType} [notification.type='info'] - The type of the notification
|
||||
* @param {boolean} [notification.autoClose=true] - Whether the notification should auto-close
|
||||
* @param {Promise} [notification.promise] - A promise to track for loading, success, and error states
|
||||
* @param {object} [notification.promiseMessages] - Custom messages for promise states
|
||||
* @param {string} [notification.promiseMessages.loading] - Message to show while promise is pending
|
||||
* @param {string | function} [notification.promiseMessages.success] - Message to show when promise resolves
|
||||
* @param {string | function} [notification.promiseMessages.error] - Message to show when promise rejects
|
||||
* @returns {string} id - The ID of the created notification
|
||||
*/
|
||||
show({
|
||||
title,
|
||||
message,
|
||||
duration = 5000,
|
||||
position = 'bottomRight',
|
||||
position = 'bottom-right',
|
||||
type = 'info',
|
||||
autoClose = true,
|
||||
}) {
|
||||
promise,
|
||||
promiseMessages,
|
||||
}: {
|
||||
title: string;
|
||||
message: string | ((data?: any) => string);
|
||||
duration?: number;
|
||||
position?:
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right'
|
||||
| 'top-center'
|
||||
| 'bottom-center';
|
||||
type?: ToastType;
|
||||
autoClose?: boolean;
|
||||
promise?: Promise<any>;
|
||||
promiseMessages?: {
|
||||
loading?: string;
|
||||
success?: string | ((data: any) => string);
|
||||
error?: string | ((error: any) => string);
|
||||
};
|
||||
}): string {
|
||||
if (promise && promiseMessages) {
|
||||
const loadingId = serviceImplementation._show({
|
||||
title,
|
||||
message: promiseMessages.loading || 'Loading...',
|
||||
type: 'loading',
|
||||
autoClose: false,
|
||||
position,
|
||||
});
|
||||
|
||||
promise.then(
|
||||
data => {
|
||||
const successMessage =
|
||||
typeof promiseMessages.success === 'function'
|
||||
? promiseMessages.success(data)
|
||||
: promiseMessages.success || 'Success';
|
||||
|
||||
serviceImplementation._show({
|
||||
title,
|
||||
message: successMessage,
|
||||
type: 'success',
|
||||
duration,
|
||||
position,
|
||||
autoClose,
|
||||
});
|
||||
this.hide(loadingId);
|
||||
},
|
||||
error => {
|
||||
const errorMessage =
|
||||
typeof promiseMessages.error === 'function'
|
||||
? promiseMessages.error(error)
|
||||
: promiseMessages.error || 'Error';
|
||||
|
||||
serviceImplementation._show({
|
||||
title,
|
||||
message: errorMessage,
|
||||
type: 'error',
|
||||
duration,
|
||||
position,
|
||||
autoClose,
|
||||
});
|
||||
this.hide(loadingId);
|
||||
}
|
||||
);
|
||||
|
||||
return loadingId;
|
||||
}
|
||||
|
||||
return serviceImplementation._show({
|
||||
title,
|
||||
message,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import { getPresentationIds, PresentationIds } from './getPresentationIds';
|
||||
|
||||
class ViewportGridService extends PubSubService {
|
||||
public static readonly EVENTS = {
|
||||
@@ -13,18 +12,75 @@ class ViewportGridService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'viewportGridService',
|
||||
altName: 'ViewportGridService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new ViewportGridService();
|
||||
create: ({ configuration = {}, servicesManager }) => {
|
||||
return new ViewportGridService({ servicesManager });
|
||||
},
|
||||
};
|
||||
|
||||
public static getPresentationIds = getPresentationIds;
|
||||
|
||||
serviceImplementation = {};
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
presentationIdProviders: Map<
|
||||
string,
|
||||
(id: string, { viewport, viewports, isUpdatingSameViewport, servicesManager }) => unknown
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
constructor({ servicesManager }) {
|
||||
super(ViewportGridService.EVENTS);
|
||||
this.servicesManager = servicesManager;
|
||||
this.serviceImplementation = {};
|
||||
this.presentationIdProviders = new Map();
|
||||
}
|
||||
|
||||
public addPresentationIdProvider(
|
||||
id: string,
|
||||
provider: (id: string, { viewport, viewports, isUpdatingSameViewport }) => unknown
|
||||
): void {
|
||||
this.presentationIdProviders.set(id, provider);
|
||||
}
|
||||
|
||||
public getPresentationId(id: string, viewportId: string): string | null {
|
||||
const state = this.getState();
|
||||
const viewport = state.viewports.get(viewportId);
|
||||
return this._getPresentationId(id, {
|
||||
viewport,
|
||||
viewports: state.viewports,
|
||||
});
|
||||
}
|
||||
|
||||
private _getPresentationId(id, { viewport, viewports }) {
|
||||
const isUpdatingSameViewport = [...viewports.values()].some(
|
||||
v =>
|
||||
v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
|
||||
v.viewportId === viewport.viewportId
|
||||
);
|
||||
|
||||
const provider = this.presentationIdProviders.get(id);
|
||||
if (provider) {
|
||||
const result = provider(id, {
|
||||
viewport,
|
||||
viewports,
|
||||
isUpdatingSameViewport,
|
||||
servicesManager: this.servicesManager,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getPresentationIds({ viewport, viewports }) {
|
||||
// Use the keys of the Map to get all registered provider IDs
|
||||
const registeredPresentationProviders = Array.from(this.presentationIdProviders.keys());
|
||||
|
||||
return registeredPresentationProviders.reduce((acc, id) => {
|
||||
const value = this._getPresentationId(id, {
|
||||
viewport,
|
||||
viewports,
|
||||
});
|
||||
if (value !== null) {
|
||||
acc[id] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
@@ -75,15 +131,24 @@ class ViewportGridService extends PubSubService {
|
||||
|
||||
public setActiveViewportId(id: string) {
|
||||
this.serviceImplementation._setActiveViewport(id);
|
||||
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
|
||||
viewportId: id,
|
||||
});
|
||||
|
||||
// Use queueMicrotask to delay the event broadcast
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
|
||||
viewportId: id,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public getState() {
|
||||
public getState(): AppTypes.ViewportGrid.State {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public getViewportState(viewportId: string) {
|
||||
const state = this.getState();
|
||||
return state.viewports.get(viewportId);
|
||||
}
|
||||
|
||||
public setViewportIsReady(viewportId, callback) {
|
||||
this.serviceImplementation._setViewportIsReady(viewportId, callback);
|
||||
}
|
||||
@@ -108,19 +173,34 @@ class ViewportGridService extends PubSubService {
|
||||
public async setDisplaySetsForViewports(props) {
|
||||
await this.serviceImplementation._setDisplaySetsForViewports(props);
|
||||
const state = this.getState();
|
||||
const viewports = [];
|
||||
const updatedViewports = [];
|
||||
|
||||
const removedViewportIds = [];
|
||||
|
||||
for (const viewport of props) {
|
||||
const updatedViewport = state.viewports.get(viewport.viewportId);
|
||||
|
||||
if (updatedViewport) {
|
||||
viewports.push(updatedViewport);
|
||||
updatedViewports.push(updatedViewport);
|
||||
|
||||
const updatedDisplaySetUIDs = updatedViewport.displaySetInstanceUIDs || [];
|
||||
|
||||
const isCleared = updatedDisplaySetUIDs.length === 0;
|
||||
|
||||
if (isCleared) {
|
||||
removedViewportIds.push(viewport.viewportId);
|
||||
}
|
||||
} else {
|
||||
console.warn("ViewportGridService::Didn't find updated viewport", viewport);
|
||||
removedViewportIds.push(viewport.viewportId);
|
||||
}
|
||||
}
|
||||
this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
|
||||
state,
|
||||
viewports,
|
||||
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
|
||||
state,
|
||||
viewports: updatedViewports,
|
||||
removedViewportIds,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +223,7 @@ class ViewportGridService extends PubSubService {
|
||||
* options that is initially provided as {} (eg to store intermediate state)
|
||||
* The function returns a viewport object to use at the given position.
|
||||
*/
|
||||
public setLayout({
|
||||
public async setLayout({
|
||||
numCols,
|
||||
numRows,
|
||||
layoutOptions,
|
||||
@@ -152,7 +232,11 @@ class ViewportGridService extends PubSubService {
|
||||
findOrCreateViewport = undefined,
|
||||
isHangingProtocolLayout = false,
|
||||
}) {
|
||||
this.serviceImplementation._setLayout({
|
||||
// Get the previous state before the layout change
|
||||
const prevState = this.getState();
|
||||
const prevViewportIds = new Set(prevState.viewports.keys());
|
||||
|
||||
await this.serviceImplementation._setLayout({
|
||||
numCols,
|
||||
numRows,
|
||||
layoutOptions,
|
||||
@@ -161,10 +245,26 @@ class ViewportGridService extends PubSubService {
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
});
|
||||
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
|
||||
numCols,
|
||||
numRows,
|
||||
});
|
||||
|
||||
// Use queueMicrotask to ensure the layout changed event is published after
|
||||
setTimeout(() => {
|
||||
// Get the new state after the layout change
|
||||
const state = this.getState();
|
||||
const currentViewportIds = new Set(state.viewports.keys());
|
||||
|
||||
// Determine which viewport IDs have been removed
|
||||
const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id));
|
||||
|
||||
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
|
||||
numCols,
|
||||
numRows,
|
||||
});
|
||||
|
||||
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
|
||||
state,
|
||||
removedViewportIds,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public reset() {
|
||||
@@ -181,11 +281,23 @@ class ViewportGridService extends PubSubService {
|
||||
this.serviceImplementation._onModeExit();
|
||||
}
|
||||
|
||||
public set(state) {
|
||||
this.serviceImplementation._set(state);
|
||||
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
|
||||
state,
|
||||
});
|
||||
public set(newState) {
|
||||
const prevState = this.getState();
|
||||
const prevViewportIds = new Set(prevState.viewports.keys());
|
||||
|
||||
this.serviceImplementation._set(newState);
|
||||
|
||||
const state = this.getState();
|
||||
const currentViewportIds = new Set(state.viewports.keys());
|
||||
|
||||
const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id));
|
||||
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
|
||||
state,
|
||||
removedViewportIds,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public getNumViewportPanes() {
|
||||
@@ -207,5 +319,3 @@ class ViewportGridService extends PubSubService {
|
||||
}
|
||||
|
||||
export default ViewportGridService;
|
||||
|
||||
export type { PresentationIds };
|
||||
@@ -1,153 +0,0 @@
|
||||
const JOIN_STR = '&';
|
||||
|
||||
// The default lut presentation id if none defined
|
||||
const DEFAULT = 'default';
|
||||
|
||||
// This code finds the first unique index to add to the presentation id so that
|
||||
// two viewports containing the same display set in the same type of viewport
|
||||
// can have different presentation information. This allows comparison of
|
||||
// a single display set in two or more viewports, when the user has simply
|
||||
// dragged and dropped the view in twice. For example, it allows displaying
|
||||
// bone, brain and soft tissue views of a single display set, and to still
|
||||
// remember the specific changes to each viewport.
|
||||
const addUniqueIndex = (arr, key, viewports, isUpdatingSameViewport) => {
|
||||
arr.push(0);
|
||||
|
||||
// If we are updating the viewport, we should not increment the index
|
||||
if (isUpdatingSameViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The 128 is just a value that is larger than how many viewports we
|
||||
// display at once, used as an upper bound on how many unique presentation
|
||||
// ID's might exist for a single display set at once.
|
||||
for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
|
||||
arr[arr.length - 1] = displayInstance;
|
||||
const testId = arr.join(JOIN_STR);
|
||||
if (
|
||||
!Array.from(viewports.values()).find(
|
||||
viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
|
||||
)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getLutId = (ds): string => {
|
||||
if (!ds || !ds.options) {
|
||||
return DEFAULT;
|
||||
}
|
||||
if (ds.options.id) {
|
||||
return ds.options.id;
|
||||
}
|
||||
const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
|
||||
if (!arr.length) {
|
||||
return DEFAULT;
|
||||
}
|
||||
return arr.join(JOIN_STR);
|
||||
};
|
||||
|
||||
export type PresentationIds = {
|
||||
positionPresentationId?: string;
|
||||
lutPresentationId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a set of presentation IDs for a viewport. The presentation IDs are
|
||||
* used to remember the presentation state of the viewport when it is navigated
|
||||
* to different layouts.
|
||||
*
|
||||
* The design of this is setup to allow preserving the view information in the
|
||||
* following cases:
|
||||
*
|
||||
*
|
||||
* * If a set of display sets was previously displayed in the same initial
|
||||
* position as it is currently being asked to be displayed,
|
||||
* then remember the camera position as previously displayed
|
||||
*
|
||||
* * If a set of display sets was previously displayed with the same initial
|
||||
* LUT conditions, then remember the last LUT displayed for that display set
|
||||
* and re-apply it.
|
||||
*
|
||||
* * Otherwise, apply the initial hanging protocol specified LUT and camera
|
||||
* position to new display sets.
|
||||
*
|
||||
* This means generating two presentationId keys:
|
||||
*
|
||||
* `positionPresentationId`
|
||||
*
|
||||
* Used for getting the camera/initial position state sync values.
|
||||
* This is a combination of:
|
||||
* * `viewportOptions.id`
|
||||
* * `viewportOptions.orientation`
|
||||
* * display set UID's - as displayed for this viewport, excluding seg
|
||||
* * a unique index number if the previous key is already displayed
|
||||
*
|
||||
* `lutPresentationId`
|
||||
*
|
||||
* Used for getting the voi LUT information. Generated from:
|
||||
*
|
||||
* * `displaySetOption[0].options` - including the id if present
|
||||
* * displaySetUID's
|
||||
* * a unique index number if the previously generated key is already
|
||||
* displayed.
|
||||
*
|
||||
* @param viewport requiring a presentation Id
|
||||
* @param viewports is the list of viewports being shown. Any presentation ID's
|
||||
* among them must not be re-used in order to have each viewport have it's own presentation ID.
|
||||
* @returns PresentationIds
|
||||
*/
|
||||
const getPresentationIds = (viewport, viewports): PresentationIds => {
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
const { viewportOptions, displaySetInstanceUIDs, displaySetOptions } = viewport;
|
||||
if (!viewportOptions || !displaySetInstanceUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, orientation } = viewportOptions;
|
||||
const lutId = getLutId(displaySetOptions[0]);
|
||||
const lutPresentationArr = [lutId];
|
||||
|
||||
const positionPresentationArr = [orientation || 'acquisition'];
|
||||
if (id) {
|
||||
positionPresentationArr.push(id);
|
||||
}
|
||||
|
||||
if (displaySetOptions.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) {
|
||||
positionPresentationArr.push(`custom`);
|
||||
}
|
||||
|
||||
for (const uid of displaySetInstanceUIDs) {
|
||||
positionPresentationArr.push(uid);
|
||||
lutPresentationArr.push(uid);
|
||||
}
|
||||
|
||||
// only add unique index if the viewport is getting inserted and not updated
|
||||
const isUpdatingSameViewport = Array.from(viewports.values()).some(v => {
|
||||
return (
|
||||
v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
|
||||
v.viewportId === viewport.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
// if it is updating the viewport we should not increment the index since
|
||||
// it might be a layer on the fusion or a SEG layer that is added on
|
||||
// top of the original display set
|
||||
addUniqueIndex(
|
||||
positionPresentationArr,
|
||||
'positionPresentationId',
|
||||
viewports,
|
||||
isUpdatingSameViewport
|
||||
);
|
||||
addUniqueIndex(lutPresentationArr, 'lutPresentationId', viewports, isUpdatingSameViewport);
|
||||
|
||||
const lutPresentationId = lutPresentationArr.join(JOIN_STR);
|
||||
const positionPresentationId = positionPresentationArr.join(JOIN_STR);
|
||||
return { lutPresentationId, positionPresentationId };
|
||||
};
|
||||
|
||||
export default getPresentationIds;
|
||||
export { getPresentationIds };
|
||||
@@ -1,5 +1,3 @@
|
||||
import ViewportGridService from './ViewportGridService';
|
||||
import type { PresentationIds } from './ViewportGridService';
|
||||
|
||||
export default ViewportGridService;
|
||||
export type { PresentationIds };
|
||||
@@ -135,6 +135,7 @@ class WorkflowStepsService extends PubSubService {
|
||||
const toUse = Array.isArray(toolbarButtons) ? toolbarButtons : [toolbarButtons];
|
||||
|
||||
toUse.forEach(({ buttonSection, buttons }) => {
|
||||
toolbarService.clearButtonSection(buttonSection);
|
||||
toolbarService.createButtonSection(buttonSection, buttons);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import HangingProtocolService from './HangingProtocolService';
|
||||
import pubSubServiceInterface, { PubSubService } from './_shared/pubSubServiceInterface';
|
||||
import UserAuthenticationService from './UserAuthenticationService';
|
||||
import CustomizationService from './CustomizationService';
|
||||
import StateSyncService from './StateSyncService';
|
||||
import PanelService from './PanelService';
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
import StudyPrefetcherService from './StudyPrefetcherService';
|
||||
@@ -27,7 +26,6 @@ export {
|
||||
ServicesManager,
|
||||
ServiceProvidersManager,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
UINotificationService,
|
||||
|
||||
@@ -5,7 +5,6 @@ import MeasurementServiceType from '../services/MeasurementService';
|
||||
import ViewportGridServiceType from '../services/ViewportGridService';
|
||||
import ToolbarServiceType from '../services/ToolBarService';
|
||||
import DisplaySetServiceType from '../services/DisplaySetService';
|
||||
import StateSyncServiceType from '../services/StateSyncService';
|
||||
import UINotificationServiceType from '../services/UINotificationService';
|
||||
import UIModalServiceType from '../services/UIModalService';
|
||||
import WorkflowStepsServiceType from '../services/WorkflowStepsService';
|
||||
@@ -22,6 +21,18 @@ import ExtensionManagerType from '../extensions/ExtensionManager';
|
||||
|
||||
import Hotkey from '../classes/Hotkey';
|
||||
|
||||
import * as CommandTypes from './Command';
|
||||
import * as ColorTypes from './Color';
|
||||
import * as ConsumerTypes from './Consumer';
|
||||
import * as DataSourceTypes from './DataSource';
|
||||
import * as DataSourceConfigurationAPITypes from './DataSourceConfigurationAPI';
|
||||
import * as DisplaySetTypes from './DisplaySet';
|
||||
import * as HangingProtocolTypes from './HangingProtocol';
|
||||
import * as IPubSubTypes from './IPubSub';
|
||||
import * as PanelModuleTypes from './PanelModule';
|
||||
import * as StudyMetadataTypes from './StudyMetadata';
|
||||
import * as ViewportGridTypes from './ViewportGridType';
|
||||
|
||||
import { StepOptions, TourOptions } from 'shepherd.js';
|
||||
|
||||
declare global {
|
||||
@@ -37,7 +48,6 @@ declare global {
|
||||
export type ViewportGridService = ViewportGridServiceType;
|
||||
export type UIModalService = UIModalServiceType;
|
||||
export type UINotificationService = UINotificationServiceType;
|
||||
export type StateSyncService = StateSyncServiceType;
|
||||
export type WorkflowStepsService = WorkflowStepsServiceType;
|
||||
export type CineService = CineServiceType;
|
||||
export type UserAuthenticationService = UserAuthenticationServiceType;
|
||||
@@ -61,7 +71,6 @@ declare global {
|
||||
viewportGridService?: ViewportGridServiceType;
|
||||
uiModalService?: UIModalServiceType;
|
||||
uiNotificationService?: UINotificationServiceType;
|
||||
stateSyncService?: StateSyncServiceType;
|
||||
workflowStepsService?: WorkflowStepsServiceType;
|
||||
cineService?: CineServiceType;
|
||||
userAuthenticationService?: UserAuthenticationServiceType;
|
||||
@@ -70,15 +79,15 @@ declare global {
|
||||
panelService?: PanelServiceType;
|
||||
studyPrefetcherService?: StudyPrefetcherServiceType;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
routerBasename?: string;
|
||||
customizationService?: any;
|
||||
customizationService?: CustomizationServiceType;
|
||||
extensions?: string[];
|
||||
modes?: string[];
|
||||
experimentalStudyBrowserSort?: boolean;
|
||||
defaultDataSourceName?: string;
|
||||
hotkeys?: Record<string, Hotkey> | Hotkey[];
|
||||
useSharedArrayBuffer?: 'AUTO' | 'FALSE' | 'TRUE';
|
||||
preferSizeOverAccuracy?: boolean;
|
||||
useNorm16Texture?: boolean;
|
||||
useCPURendering?: boolean;
|
||||
@@ -92,8 +101,8 @@ declare global {
|
||||
interaction?: number;
|
||||
prefetch?: number;
|
||||
thumbnail?: number;
|
||||
compute?: number;
|
||||
};
|
||||
disableEditing?: boolean;
|
||||
maxNumberOfWebWorkers?: number;
|
||||
acceptHeader?: string[];
|
||||
investigationalUseDialog?: {
|
||||
@@ -114,18 +123,21 @@ declare global {
|
||||
activateViewportBeforeInteraction?: boolean;
|
||||
autoPlayCine?: boolean;
|
||||
showStudyList?: boolean;
|
||||
whiteLabeling?: any;
|
||||
httpErrorHandler?: any;
|
||||
whiteLabeling?: Record<string, unknown>;
|
||||
httpErrorHandler?: (error: Error) => void;
|
||||
addWindowLevelActionMenu?: boolean;
|
||||
dangerouslyUseDynamicConfig?: {
|
||||
enabled: boolean;
|
||||
regex: RegExp;
|
||||
};
|
||||
onConfiguration?: (dicomWebConfig: any, options: any) => any;
|
||||
dataSources?: any;
|
||||
oidc?: any;
|
||||
peerImport?: (moduleId: string) => Promise<any>;
|
||||
studyPrefetcher: {
|
||||
onConfiguration?: (
|
||||
dicomWebConfig: Record<string, unknown>,
|
||||
options: Record<string, unknown>
|
||||
) => Record<string, unknown>;
|
||||
dataSources?: Record<string, unknown>;
|
||||
oidc?: Record<string, unknown>;
|
||||
peerImport?: (moduleId: string) => Promise<Record<string, unknown>>;
|
||||
studyPrefetcher?: {
|
||||
enabled: boolean;
|
||||
displaySetsCount: number;
|
||||
maxNumPrefetchRequests: number;
|
||||
@@ -145,13 +157,108 @@ declare global {
|
||||
extensionManager?: ExtensionManager;
|
||||
config?: Config;
|
||||
}
|
||||
|
||||
// Add Command namespace to AppTypes
|
||||
export namespace Commands {
|
||||
export type SimpleCommand = CommandTypes.SimpleCommand;
|
||||
export type ComplexCommand = CommandTypes.ComplexCommand;
|
||||
export type Command = CommandTypes.Command;
|
||||
export type RunCommand = CommandTypes.RunCommand;
|
||||
export interface Commands extends CommandTypes.Commands {}
|
||||
}
|
||||
|
||||
// Color types
|
||||
export type RGB = ColorTypes.RGB;
|
||||
|
||||
// Consumer types
|
||||
export type Consumer = ConsumerTypes.Consumer;
|
||||
|
||||
// DataSource types
|
||||
export type DataSourceDefinition = DataSourceTypes.DataSourceDefinition;
|
||||
|
||||
// DataSourceConfigurationAPI types
|
||||
export namespace DataSourceConfiguration {
|
||||
export type BaseDataSourceConfigurationAPIItem =
|
||||
DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPIItem;
|
||||
export type BaseDataSourceConfigurationAPI =
|
||||
DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPI;
|
||||
}
|
||||
|
||||
// DisplaySet types
|
||||
export type DisplaySet = DisplaySetTypes.DisplaySet;
|
||||
export type DisplaySetSeriesMetadataInvalidatedEvent =
|
||||
DisplaySetTypes.DisplaySetSeriesMetadataInvalidatedEvent;
|
||||
|
||||
// HangingProtocol types
|
||||
export namespace HangingProtocol {
|
||||
export type DisplaySetInfo = HangingProtocolTypes.DisplaySetInfo;
|
||||
export type ViewportMatchDetails = HangingProtocolTypes.ViewportMatchDetails;
|
||||
export type DisplaySetMatchDetails = HangingProtocolTypes.DisplaySetMatchDetails;
|
||||
export type DisplaySetAndViewportOptions = HangingProtocolTypes.DisplaySetAndViewportOptions;
|
||||
export type DisplayArea = HangingProtocolTypes.DisplayArea;
|
||||
export type SetProtocolOptions = HangingProtocolTypes.SetProtocolOptions;
|
||||
export type HangingProtocolMatchDetails = HangingProtocolTypes.HangingProtocolMatchDetails;
|
||||
export type ConstraintValue = HangingProtocolTypes.ConstraintValue;
|
||||
export type Constraint = HangingProtocolTypes.Constraint;
|
||||
export type MatchingRule = HangingProtocolTypes.MatchingRule;
|
||||
export type ViewportLayoutOptions = HangingProtocolTypes.ViewportLayoutOptions;
|
||||
export type ViewportStructure = HangingProtocolTypes.ViewportStructure;
|
||||
export type DisplaySetSelector = HangingProtocolTypes.DisplaySetSelector;
|
||||
export type SyncGroup = HangingProtocolTypes.SyncGroup;
|
||||
export type CustomOptionAttribute<T> = HangingProtocolTypes.CustomOptionAttribute<T>;
|
||||
export type CustomOption<T> = HangingProtocolTypes.CustomOption<T>;
|
||||
export type initialImageOptions = HangingProtocolTypes.initialImageOptions;
|
||||
export type ViewportOptions = HangingProtocolTypes.ViewportOptions;
|
||||
export type DisplaySetOptions = HangingProtocolTypes.DisplaySetOptions;
|
||||
export type Viewport = HangingProtocolTypes.Viewport;
|
||||
export type StageStatus = HangingProtocolTypes.StageStatus;
|
||||
export type StageActivation = HangingProtocolTypes.StageActivation;
|
||||
export type ProtocolStage = HangingProtocolTypes.ProtocolStage;
|
||||
export type ProtocolNotifications = HangingProtocolTypes.ProtocolNotifications;
|
||||
export type Protocol = HangingProtocolTypes.Protocol;
|
||||
export type ProtocolGenerator = HangingProtocolTypes.ProtocolGenerator;
|
||||
export type HPInfo = HangingProtocolTypes.HPInfo;
|
||||
}
|
||||
|
||||
// IPubSub types
|
||||
export namespace PubSub {
|
||||
export type IPubSub = IPubSubTypes.default;
|
||||
export type Subscription = IPubSubTypes.Subscription;
|
||||
}
|
||||
|
||||
// PanelModule types
|
||||
export namespace PanelModule {
|
||||
export type Panel = PanelModuleTypes.Panel;
|
||||
export type ActivatePanelTriggers = PanelModuleTypes.ActivatePanelTriggers;
|
||||
export type PanelEvent = PanelModuleTypes.PanelEvent;
|
||||
export type ActivatePanelEvent = PanelModuleTypes.ActivatePanelEvent;
|
||||
}
|
||||
|
||||
// StudyMetadata types
|
||||
export namespace StudyMetadata {
|
||||
export type PatientMetadata = StudyMetadataTypes.PatientMetadata;
|
||||
export type StudyMetadata = StudyMetadataTypes.StudyMetadata;
|
||||
export type SeriesMetadata = StudyMetadataTypes.SeriesMetadata;
|
||||
export type InstanceMetadata = StudyMetadataTypes.InstanceMetadata;
|
||||
}
|
||||
|
||||
// ViewportGrid types
|
||||
export namespace ViewportGrid {
|
||||
export type Viewport = ViewportGridTypes.GridViewport;
|
||||
export type Layout = ViewportGridTypes.Layout;
|
||||
export type State = ViewportGridTypes.ViewportGridState;
|
||||
export type Viewports = ViewportGridTypes.GridViewports;
|
||||
export type GridViewportOptions = ViewportGridTypes.GridViewportOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PresentationIds {}
|
||||
|
||||
export type withAppTypes<T = object> = T &
|
||||
AppTypes.Services &
|
||||
AppTypes.Managers & {
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
} & AppTypes.Config;
|
||||
|
||||
export type withTestTypes<T = object> = T & AppTypes.Test;
|
||||
}
|
||||
@@ -5,8 +5,13 @@ export type DisplaySet = {
|
||||
instances: InstanceMetadata[];
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID?: string;
|
||||
SeriesNumber?: number;
|
||||
SeriesDescription?: string;
|
||||
numImages?: number;
|
||||
unsupported?: boolean;
|
||||
Modality?: string;
|
||||
imageIds?: string[];
|
||||
images?: unknown[];
|
||||
};
|
||||
|
||||
export type DisplaySetSeriesMetadataInvalidatedEvent = {
|
||||
|
||||
@@ -81,6 +81,7 @@ export type ConstraintValue =
|
||||
| number
|
||||
| boolean
|
||||
| []
|
||||
| string[]
|
||||
| {
|
||||
value: string | number | boolean | [];
|
||||
};
|
||||
@@ -92,6 +93,7 @@ export type Constraint = {
|
||||
// A caseless contains
|
||||
containsI?: string;
|
||||
contains?: ConstraintValue;
|
||||
doesNotContain?: ConstraintValue;
|
||||
greaterThan?: ConstraintValue;
|
||||
};
|
||||
|
||||
@@ -101,7 +103,7 @@ export type MatchingRule = {
|
||||
// Defaults to 1
|
||||
weight?: number;
|
||||
attribute: string;
|
||||
constraint: Constraint;
|
||||
constraint?: Constraint;
|
||||
// Not required by default
|
||||
required?: boolean;
|
||||
};
|
||||
@@ -140,6 +142,11 @@ export type DisplaySetSelector = {
|
||||
studyMatchingRules?: MatchingRule[];
|
||||
};
|
||||
|
||||
export type OverlaySelector = {
|
||||
id?: string;
|
||||
matchingRules: MatchingRule[];
|
||||
};
|
||||
|
||||
export type SyncGroup = {
|
||||
type: string;
|
||||
id: string;
|
||||
@@ -166,6 +173,7 @@ export type ViewportOptions = {
|
||||
viewportType?: CustomOption<string>;
|
||||
id?: string;
|
||||
orientation?: CustomOption<string>;
|
||||
background?: CustomOption<[number, number, number]>;
|
||||
viewportId?: string;
|
||||
displayArea?: DisplayArea;
|
||||
initialImageOptions?: CustomOption<initialImageOptions>;
|
||||
@@ -189,9 +197,17 @@ export type DisplaySetOptions = {
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
// some options for overlays
|
||||
// such as segmentation options
|
||||
export type OverlayOptions = {
|
||||
id?: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type Viewport = {
|
||||
viewportOptions: ViewportOptions;
|
||||
displaySets: DisplaySetOptions[];
|
||||
overlays?: OverlayOptions[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -271,7 +287,7 @@ export type ProtocolNotifications = {
|
||||
/**
|
||||
* A protocol is the top level definition for a hanging protocol.
|
||||
* It is a set of rules about when the protocol can be applied at all,
|
||||
* as well as a set of stages that represent indivividual views.
|
||||
* as well as a set of stages that represent individual views.
|
||||
* Additionally, the display set selectors are used to choose from the existing
|
||||
* display sets. The hanging protocol definition here does NOT allow
|
||||
* redefining the display sets to use, but only selects the views to show.
|
||||
@@ -283,6 +299,8 @@ export type Protocol = {
|
||||
description?: string;
|
||||
/** Maps ids to display set selectors to choose display sets */
|
||||
displaySetSelectors: Record<string, DisplaySetSelector>;
|
||||
/** overlay selectors that decide whether an overlay such as segmentation should be shown or not */
|
||||
overlaySelectors?: Record<string, OverlaySelector>;
|
||||
/** A default viewport to use for any stage to select new viewport layouts. */
|
||||
defaultViewport?: Viewport;
|
||||
stages: ProtocolStage[];
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
ViewportGridService,
|
||||
ToolbarService,
|
||||
DisplaySetService,
|
||||
StateSyncService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
WorkflowStepsService,
|
||||
@@ -29,7 +28,6 @@ interface Services {
|
||||
viewportGridService?: ViewportGridService;
|
||||
uiModalService?: UIModalService;
|
||||
uiNotificationService?: UINotificationService;
|
||||
stateSyncService?: StateSyncService;
|
||||
workflowStepsService: WorkflowStepsService;
|
||||
cineService?: CineService;
|
||||
userAuthenticationService?: UserAuthenticationService;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
export interface GridViewportOptions {
|
||||
id?: string;
|
||||
viewportId?: string;
|
||||
viewportType?: string;
|
||||
toolGroupId?: string;
|
||||
presentationIds?: AppTypes.PresentationIds;
|
||||
flipHorizontal?: boolean;
|
||||
//
|
||||
orientation?: string;
|
||||
allowUnmatchedView?: boolean;
|
||||
needsRerendering?: boolean;
|
||||
background?: [number, number, number];
|
||||
syncGroups?: unknown[];
|
||||
rotation?: number;
|
||||
initialImageOptions?: unknown;
|
||||
customViewportProps?: Record<string, unknown>;
|
||||
//
|
||||
displayArea?: unknown;
|
||||
viewReference?: unknown;
|
||||
}
|
||||
|
||||
export interface GridViewport {
|
||||
viewportId: string;
|
||||
displaySetInstanceUIDs: string[];
|
||||
viewportOptions: GridViewportOptions;
|
||||
displaySetSelectors: unknown[];
|
||||
displaySetOptions: unknown[];
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
viewportLabel: string | null;
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
export interface Layout {
|
||||
numRows: number;
|
||||
numCols: number;
|
||||
layoutType: string;
|
||||
}
|
||||
|
||||
export type GridViewports = Map<string, GridViewport>;
|
||||
|
||||
export interface ViewportGridState {
|
||||
activeViewportId: string | null;
|
||||
layout: Layout;
|
||||
isHangingProtocolLayout: boolean;
|
||||
viewports: GridViewports;
|
||||
}
|
||||
|
||||
export type SetDisplaySetsForViewportsProps = Array<{
|
||||
viewportId: string;
|
||||
displaySetInstanceUIDs: string[];
|
||||
viewportOptions?: AppTypes.ViewportGrid.GridViewportOptions;
|
||||
displaySetOptions?: Array<{
|
||||
id?: string;
|
||||
voi?: {
|
||||
windowWidth: number;
|
||||
windowCenter: number;
|
||||
};
|
||||
voiInverted?: boolean;
|
||||
blendMode?: string;
|
||||
slabThickness?: number;
|
||||
colormap?: {
|
||||
name: string;
|
||||
opacity?: number;
|
||||
};
|
||||
displayPreset?: string;
|
||||
}>;
|
||||
}>;
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
BaseDataSourceConfigurationAPI,
|
||||
BaseDataSourceConfigurationAPIItem,
|
||||
} from './DataSourceConfigurationAPI';
|
||||
import type DisplaySet from '../services/DisplaySetService/DisplaySet';
|
||||
|
||||
export type * from '../services/ViewportGridService';
|
||||
export type * from '../services/CustomizationService/types';
|
||||
@@ -32,5 +31,4 @@ export {
|
||||
DataSourceDefinition,
|
||||
BaseDataSourceConfigurationAPI,
|
||||
BaseDataSourceConfigurationAPIItem,
|
||||
DisplaySet,
|
||||
};
|
||||
@@ -50,7 +50,10 @@ const combineFrameInstance = (frame, instance) => {
|
||||
|
||||
// Todo: we should cache this combined instance somewhere, maybe add it
|
||||
// back to the dicomMetaStore so we don't have to do this again.
|
||||
return newInstance;
|
||||
return {
|
||||
...newInstance,
|
||||
ImagePositionPatient: newInstance.ImagePositionPatient ?? [0, 0, frameNumber],
|
||||
};
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
* @returns tabs - The prop object expected by the StudyBrowser component
|
||||
*/
|
||||
|
||||
export function createStudyBrowserTabs(primaryStudyInstanceUIDs, studyDisplayList, displaySets, recentTimeframeMS = 31536000000) {
|
||||
export function createStudyBrowserTabs(
|
||||
primaryStudyInstanceUIDs,
|
||||
studyDisplayList,
|
||||
displaySets,
|
||||
recentTimeframeMS = 31536000000
|
||||
) {
|
||||
const primaryStudies = [];
|
||||
const allStudies = [];
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ export default function getImageId(instance, frame, thumbnail = false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.imageId && frame === undefined) {
|
||||
return instance.imageId;
|
||||
}
|
||||
|
||||
if (typeof instance.getImageId === 'function') {
|
||||
return instance.getImageId();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ describe('Top level exports', () => {
|
||||
'resolveObjectPath',
|
||||
'hierarchicalListUtils',
|
||||
'progressTrackingUtils',
|
||||
'subscribeToNextViewportGridChange',
|
||||
'uuidv4',
|
||||
'addAccessors',
|
||||
].sort();
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
sortingCriteria,
|
||||
seriesSortCriteria,
|
||||
} from './sortStudy';
|
||||
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
|
||||
import { splitComma, getSplitParam } from './splitComma';
|
||||
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
|
||||
import { sopClassDictionary } from './sopClassDictionary';
|
||||
@@ -81,7 +80,6 @@ const utils = {
|
||||
debounce,
|
||||
roundNumber,
|
||||
downloadCSVReport,
|
||||
subscribeToNextViewportGridChange,
|
||||
splitComma,
|
||||
getSplitParam,
|
||||
generateAcceptHeader,
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
|
||||
function roundNumber(value, precision = 2) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => roundNumber(v, precision)).join(", ");
|
||||
return value.map(v => roundNumber(v, precision)).join(', ');
|
||||
}
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return "NaN";
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return 'NaN';
|
||||
}
|
||||
value = Number(value);
|
||||
const absValue = Math.abs(value);
|
||||
@@ -27,16 +27,16 @@ function roundNumber(value, precision = 2) {
|
||||
absValue >= 100
|
||||
? precision - 2
|
||||
: absValue >= 10
|
||||
? precision - 1
|
||||
: absValue >= 1
|
||||
? precision
|
||||
: absValue >= 0.1
|
||||
? precision + 1
|
||||
: absValue >= 0.01
|
||||
? precision + 2
|
||||
: absValue >= 0.001
|
||||
? precision + 3
|
||||
: precision + 4;
|
||||
? precision - 1
|
||||
: absValue >= 1
|
||||
? precision
|
||||
: absValue >= 0.1
|
||||
? precision + 1
|
||||
: absValue >= 0.01
|
||||
? precision + 2
|
||||
: absValue >= 0.001
|
||||
? precision + 3
|
||||
: precision + 4;
|
||||
return value.toFixed(fixedPrecision);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ViewportGridService } from '../services';
|
||||
|
||||
/**
|
||||
* Subscribes to the very next LAYOUT_CHANGED event that
|
||||
* is not currently on the event queue. The subscriptions are made on a 'zero'
|
||||
* timeout so as to avoid responding to any of those events currently on the event queue.
|
||||
* The subscription persists only for a single invocation of either event.
|
||||
* Once either event is fired, the subscriptions are unsubscribed.
|
||||
* @param viewportGridService the viewport grid service to subscribe to
|
||||
* @param gridChangeCallback the callback
|
||||
*/
|
||||
function subscribeToNextViewportGridChange(
|
||||
viewportGridService: ViewportGridService,
|
||||
gridChangeCallback: (arg: unknown) => void
|
||||
): void {
|
||||
const subscriber = () => {
|
||||
const callback = (callbackProps: unknown) => {
|
||||
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
gridChangeCallback(callbackProps);
|
||||
};
|
||||
|
||||
const subscriptions = [
|
||||
viewportGridService.subscribe(viewportGridService.EVENTS.LAYOUT_CHANGED, callback),
|
||||
];
|
||||
};
|
||||
|
||||
window.setTimeout(subscriber, 0);
|
||||
}
|
||||
|
||||
export { subscribeToNextViewportGridChange };
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
export default function toNumber(val) {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(v => (v !== undefined ? Number(v) : v));
|
||||
return [...val].map(v => (v !== undefined ? Number(v) : v));
|
||||
} else {
|
||||
return val !== undefined ? Number(val) : val;
|
||||
}
|
||||
|
||||
@@ -185,17 +185,6 @@ For DICOM video and PDF it has been found that Orthanc delivers multipart, while
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](../index.md).
|
||||
|
||||
### DICOM Upload
|
||||
See the [`dicomUploadEnabled`](#dicomuploadenabled) data source configuration option.
|
||||
|
||||
Don't forget to add the customization to the config as well
|
||||
|
||||
```js
|
||||
customizationService: {
|
||||
dicomUploadComponent:
|
||||
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
|
||||
},
|
||||
```
|
||||
|
||||
### DICOM PDF
|
||||
See the [`singlepart`](#singlepart) data source configuration option.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
sidebar_position: 13
|
||||
sidebar_label: Web Workers
|
||||
---
|
||||
# Web Worker Implementation Guide
|
||||
|
||||
## Overview
|
||||
Web Workers enable running computationally intensive tasks in background threads without blocking the UI. This guide explains how to implement them step by step.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### 1. Create Your Worker File
|
||||
First, create a worker file with your background tasks:
|
||||
|
||||
```javascript
|
||||
// myWorker.js
|
||||
import { expose } from 'comlink';
|
||||
|
||||
const obj = {
|
||||
// Simple task
|
||||
basicCalculation({ data }) {
|
||||
// Your computation here
|
||||
return result;
|
||||
},
|
||||
|
||||
// Task with progress updates
|
||||
longRunningTask({ data }, progressCallback) {
|
||||
const total = data.length;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
// Your processing logic
|
||||
|
||||
if (progressCallback) {
|
||||
const progress = Math.round((i / total) * 100);
|
||||
progressCallback(progress);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
```
|
||||
|
||||
### 2. Register the Worker
|
||||
|
||||
In the main thread, can be your service, commands module, etc.
|
||||
|
||||
```javascript
|
||||
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||
|
||||
const workerManager = getWebWorkerManager();
|
||||
|
||||
// Define worker creation function
|
||||
const workerFn = () => {
|
||||
return new Worker(
|
||||
new URL('./myWorker.js', import.meta.url),
|
||||
{ name: 'my-worker' }
|
||||
);
|
||||
};
|
||||
|
||||
// Registration options
|
||||
const options = {
|
||||
maxWorkerInstances: 1, // Number of concurrent workers
|
||||
autoTerminateOnIdle: {
|
||||
enabled: true,
|
||||
idleTimeThreshold: 3000, // Terminate after 3s idle
|
||||
},
|
||||
};
|
||||
|
||||
// Register the worker
|
||||
workerManager.registerWorker('my-worker', workerFn, options);
|
||||
```
|
||||
|
||||
:::info
|
||||
It is recommended to register the worker in top of the commands module. So that it
|
||||
gets registered before any commands that need to use the worker.
|
||||
:::
|
||||
|
||||
### 3. Execute Tasks
|
||||
|
||||
```javascript
|
||||
// Basic execution
|
||||
try {
|
||||
const result = await workerManager.executeTask(
|
||||
'my-worker',
|
||||
'basicCalculation',
|
||||
{ data: myData }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Task failed:', error);
|
||||
}
|
||||
|
||||
// Execution with progress callback
|
||||
try {
|
||||
const result = await workerManager.executeTask(
|
||||
'my-worker',
|
||||
'longRunningTask',
|
||||
{ data: myData },
|
||||
{
|
||||
callbacks: [
|
||||
(progress) => {
|
||||
console.log(`Progress: ${progress}%`);
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Task failed:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## Progress Events (Optional)
|
||||
|
||||
If you want to show progress in your UI as a loading spinner, you can implement a progress event system:
|
||||
|
||||
### 1. Publish Progress Events
|
||||
|
||||
```javascript
|
||||
// Helper to trigger progress events
|
||||
const publishProgress = (eventTarget, progress, taskId) => {
|
||||
triggerEvent(eventTarget, 'WEB_WORKER_PROGRESS', {
|
||||
progress, // number 0-100
|
||||
type: 'YOUR_TASK_TYPE', // can be any string identifier
|
||||
id: taskId, // unique task identifier
|
||||
});
|
||||
};
|
||||
|
||||
// Usage in your application
|
||||
async function runTaskWithProgress(data) {
|
||||
// Start progress
|
||||
publishProgress(eventTarget, 0, data.id);
|
||||
|
||||
try {
|
||||
const result = await workerManager.executeTask(
|
||||
'my-worker',
|
||||
'longRunningTask',
|
||||
{ data },
|
||||
{
|
||||
callbacks: [
|
||||
(progress) => {
|
||||
publishProgress(eventTarget, progress, data.id);
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
// Complete progress
|
||||
publishProgress(eventTarget, 100, data.id);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Task failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: Publishing the `WEB_WORKER_PROGRESS` event on Cornerstone's `eventTarget` will automatically trigger the built-in loading spinner. This gives users visual feedback while your worker runs in the background.
|
||||
|
||||
|
||||
## Multiple Methods in One Worker
|
||||
|
||||
You can define multiple related methods in a single worker file:
|
||||
|
||||
```javascript
|
||||
// complexWorker.js
|
||||
import { expose } from 'comlink';
|
||||
|
||||
const obj = {
|
||||
processingMethod1({ data }, progressCallback) {
|
||||
// Implementation
|
||||
},
|
||||
|
||||
processingMethod2({ data }, progressCallback) {
|
||||
// Implementation
|
||||
},
|
||||
|
||||
processingMethod3({ data }, progressCallback) {
|
||||
// Implementation
|
||||
},
|
||||
|
||||
// Shared helper methods
|
||||
_internalHelper() {
|
||||
// Helper logic
|
||||
}
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
```
|
||||
@@ -347,3 +347,19 @@ customizationService.addModeCustomizations([
|
||||
:::note
|
||||
Notice the arrays and objects, the values are arrays
|
||||
:::
|
||||
|
||||
|
||||
## How do I change the cine auto mount behavior
|
||||
|
||||
You can change the cine auto mount behavior by adding the `autoCineModalities` mode customization, the value is an array of modalities that should be mounted with cine.
|
||||
|
||||
By default the viewer will mount with cine enabled for `OT` and `US` modalities.
|
||||
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'autoCineModalities',
|
||||
modalities: ['OT', 'US'],
|
||||
},
|
||||
]);
|
||||
```
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
id: 0-general
|
||||
title: General
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
|
||||
# React 18 Migration Guide
|
||||
As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version.
|
||||
What's Changing?
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before" default>
|
||||
|
||||
```md
|
||||
- React 17
|
||||
- Using `defaultProps`
|
||||
- `babel-inline-svg` for SVG imports
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After">
|
||||
|
||||
```md
|
||||
- React 18
|
||||
- Default parameters for props
|
||||
- `svgr` for SVG imports
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Update React version:
|
||||
In your custom extensions and modes, change the version of react and react-dom to ^18.3.1.
|
||||
|
||||
## Replace defaultProps with default parameters:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before" default>
|
||||
|
||||
```jsx
|
||||
const MyComponent = ({ prop1, prop2 }) => {
|
||||
return <div>{prop1} {prop2}</div>
|
||||
}
|
||||
|
||||
MyComponent.defaultProps = {
|
||||
prop1: 'default value',
|
||||
prop2: 'default value'
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After">
|
||||
|
||||
```jsx
|
||||
const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => {
|
||||
return <div>{prop1} {prop2}</div>
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Update SVG imports:
|
||||
|
||||
You might need to update your SVG imports to use the `ReactComponent` syntax, if you want to use the old Icon component. However, we have made a significant change to how we handle Icons, read the UI Migration Guide for more information.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before" default>
|
||||
|
||||
```javascript
|
||||
import arrowDown from './../../assets/icons/arrow-down.svg';
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After">
|
||||
|
||||
```javascript
|
||||
import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg';
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Polyfill.io
|
||||
|
||||
We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-).
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Crosshairs
|
||||
|
||||
They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off.
|
||||
|
||||
To disable it, remove the configuration from the `initToolGroups` in your mode.
|
||||
|
||||
```
|
||||
{
|
||||
configuration: {
|
||||
viewportIndicators: true,
|
||||
viewportIndicatorsConfig: {
|
||||
circleRadius: 5,
|
||||
xOffset: 0.95,
|
||||
yOffset: 0.05,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## useAuthorizationCodeFlow
|
||||
|
||||
`useAuthorizationCodeFlow` config is deprecated
|
||||
|
||||
now internally we detect the authorizationCodeFlow if the response_type is equal to `code`
|
||||
|
||||
you can remove the config from the appConfig
|
||||
|
||||
---
|
||||
|
||||
## StackScrollMouseWheel -> StackScroll Tool + Mouse bindings
|
||||
|
||||
If you previously used:
|
||||
|
||||
```js
|
||||
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] }
|
||||
```
|
||||
|
||||
in your `initToolGroups`, you should now use:
|
||||
|
||||
```js
|
||||
{
|
||||
toolName: toolNames.StackScroll,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
}
|
||||
```
|
||||
|
||||
This change allows for more flexible mouse bindings and keyboard combinations.
|
||||
|
||||
## VolumeRotateMouseWheel -> VolumeRotate Tool + Mouse bindings
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
{
|
||||
toolName: toolNames.VolumeRotateMouseWheel,
|
||||
configuration: {
|
||||
rotateIncrementDegrees: 5,
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Now:
|
||||
|
||||
```js
|
||||
{
|
||||
toolName: toolNames.VolumeRotate,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
configuration: {
|
||||
rotateIncrementDegrees: 5,
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DicomUpload
|
||||
|
||||
The DICOM upload functionality in OHIF has been refactored to use the standard customization service pattern. Now you don't need to put
|
||||
|
||||
`customizationService: { dicomUploadComponent: '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', },`
|
||||
|
||||
in your config, we will automatically add that if you have `dicomUploadEnabled`
|
||||
|
||||
---
|
||||
|
||||
## Viewport and Modality Support for Toolbar Buttons
|
||||
|
||||
Previously, toolbar buttons had limited support for disabling themselves based on the active viewport type (e.g., `volume3d`, `video`, `sr`) or the modality of the displayed data (e.g., `US`, `SM`). This led to inconsistencies and sometimes enabled tools in contexts where they weren't applicable.
|
||||
|
||||
The new implementation introduces more robust and flexible evaluators to control the enabled/disabled state of toolbar buttons based on viewport types and modalities.
|
||||
|
||||
**Key Changes**
|
||||
|
||||
1. **New Evaluators:** New evaluators have been added to the `getToolbarModule`:
|
||||
- `evaluate.viewport.supported`: Disables a button if the active viewport's type is listed in the `unsupportedViewportTypes` property.
|
||||
- `evaluate.modality.supported`: Disables a button based on the modalities of the displayed data. It checks for both `unsupportedModalities` (exclusion) and `supportedModalities` (inclusion).
|
||||
2. **Removal of Legacy Evaluators:**
|
||||
- Evaluators such as `evaluate.not.sm`, `evaluate.action.not.video`, `evaluate.not3D`, and `evaluate.isUS` have been removed. Migrate your toolbar button definitions to use the new evaluators mentioned above.
|
||||
|
||||
|
||||
**Replace Legacy Evaluators:**
|
||||
- Replace `evaluate.not.sm` with:
|
||||
|
||||
```json
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['sm'],
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `evaluate.action.not.video` with:
|
||||
|
||||
```json
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video'],
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `evaluate.not3D` with:
|
||||
|
||||
```json
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['volume3d'],
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `evaluate.isUS` with:
|
||||
|
||||
```json
|
||||
{
|
||||
name: 'evaluate.modality.supported',
|
||||
supportedModalities: ['US'],
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Example Migration</summary>
|
||||
|
||||
Before:
|
||||
|
||||
```json
|
||||
evaluate: ['evaluate.cine', 'evaluate.not3D'],
|
||||
```
|
||||
|
||||
After
|
||||
|
||||
```json
|
||||
evaluate: [
|
||||
'evaluate.cine',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['volume3d'],
|
||||
},
|
||||
],
|
||||
```
|
||||
</details>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: seg-new-arch
|
||||
title: New Architecture
|
||||
---
|
||||
|
||||
|
||||
## New Architecture
|
||||
|
||||
* **Viewport-Centric Architecture**
|
||||
* Previous: Segmentations were tied to toolGroups
|
||||
* Now: Segmentations are tied directly to viewports
|
||||
* Impact: More granular control but requires significant code changes
|
||||
|
||||
* **Representation Management**
|
||||
* Previous: Required managing segmentation representation UIDs
|
||||
* Now: Uses simpler segmentationId + type combination
|
||||
* Impact: Simplified but requires API updates
|
||||
|
||||
|
||||
|
||||
If you are not familiar with the difference between a segmentation and a segmentation representation, below
|
||||
|
||||
<details>
|
||||
<summary>Read More</summary>
|
||||
|
||||
In Cornerstone3DTools, we have decoupled the concept of a Segmentation from a Segmentation Representation. This means that from one Segmentation we can create multiple Segmentation Representations. For instance, a Segmentation Representation of a 3D Labelmap, can be created from a Segmentation data, and a Segmentation Representation of a Contour can be created from the same Segmentation data. This way we have decouple the presentational aspect of a Segmentation from the underlying data.
|
||||
|
||||
|
||||
Similar relationship structure has been adapted in popular medical imaging softwares such as 3D Slicer with the addition of polymorph segmentation.
|
||||
|
||||
- https://github.com/PerkLab/PolySeg
|
||||
- https://www.slicer.org/
|
||||
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
The new architecture in Cornerstone3D 2.0 makes a clear distinction between:
|
||||
|
||||
* A segmentation (the data structure containing segments)
|
||||
* A segmentation representation (how that segmentation is visualized in a specific viewport)
|
||||
|
||||
Let's now review what has changed
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
---
|
||||
id: seg-api
|
||||
title: SegmentationService API
|
||||
---
|
||||
|
||||
|
||||
|
||||
Below we will review the changes to the API of the `SegmentationService`
|
||||
|
||||
# SegmentationService API
|
||||
|
||||
## Events
|
||||
|
||||
SEGMENTATION_UPDATED -> SEGMENTATION_MODIFIED
|
||||
|
||||
|
||||
Just a rename to match the cornerstone terminology
|
||||
|
||||
## VolumeId vs SegmentationId
|
||||
|
||||
Previously, we used the SegmentationId as the VolumeId for volume-based segmentations, which led to confusion and issues.
|
||||
|
||||
Now, we have two separate IDs: one for the segmentation and one for the volume.
|
||||
|
||||
`segmentationService.getLabelmapVolume(segmentationId)` will return the volume associated with the segmentation.
|
||||
|
||||
If your code uses `cache.getVolume(segmentationId)`, update it to use the new `getLabelmapVolume` method.
|
||||
|
||||
|
||||
## getSegmentation(segmentationId)
|
||||
|
||||
remains the same it will return the segmentation object = cornerstone segmentation object with the following properties:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Global Segmentation Data which is used for the segmentation
|
||||
*/
|
||||
type Segmentation = {
|
||||
/** segmentation id */
|
||||
segmentationId: string;
|
||||
/** segmentation label */
|
||||
label: string;
|
||||
segments: {
|
||||
[segmentIndex: number]: Segment;
|
||||
};
|
||||
/**
|
||||
* Representations of the segmentation. Each segmentation "can" be viewed
|
||||
* in various representations. For instance, if a DICOM SEG is loaded, the main
|
||||
* representation is the labelmap. However, for DICOM RT the main representation
|
||||
* is contours, and other representations can be derived from the contour (currently
|
||||
* only labelmap representation is supported)
|
||||
*/
|
||||
representationData: RepresentationsData;
|
||||
/**
|
||||
* Segmentation level stats, Note each segment can have its own stats
|
||||
* This is used for caching stats for the segmentation level
|
||||
*/
|
||||
cachedStats: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
export type Segment = {
|
||||
/** segment index */
|
||||
segmentIndex: number;
|
||||
/** segment label */
|
||||
label: string;
|
||||
/** is segment locked for editing */
|
||||
locked: boolean;
|
||||
/** cached stats for the segment, e.g., pt suv mean, max etc. */
|
||||
cachedStats: { [key: string]: unknown };
|
||||
/** is segment active for editing, at the same time only one segment can be active for editing */
|
||||
active: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Compared to Cornerstone3D 1.x</summary>
|
||||
|
||||
Previously this function was returning this
|
||||
|
||||
```js
|
||||
export type Segmentation = {
|
||||
segmentationId: string;
|
||||
type: Enums.SegmentationRepresentations;
|
||||
label: string;
|
||||
activeSegmentIndex: number;
|
||||
segmentsLocked: Set<number>;
|
||||
cachedStats: { [key: string]: number };
|
||||
segmentLabels: { [key: string]: string };
|
||||
representationData: SegmentationRepresentationData;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
As you can see `segmentLabels`, `segmentsLocked`, `activeSegmentIndex`, are all gathered under the new `segments` object. We now have support for per segment cachedStats as well.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## getSegmentations
|
||||
|
||||
It provides all segmentations in the state. Previously, it accepted a `filterNonhydrated` flag, but since we've moved away from hydration and every loaded segmentation is now hydrated by default, it returns all segmentations.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## getActiveSegmentation
|
||||
|
||||
|
||||
After migrating to viewport-specific segmentations, different viewports can have distinct active segmentations for editing. The panel will always display the active segmentation when the active viewport changes.
|
||||
|
||||
Before (3.8)
|
||||
|
||||
```js
|
||||
// Returns full segmentation object
|
||||
public getActiveSegmentation(): Segmentation {
|
||||
const segmentations = this.getSegmentations();
|
||||
return segmentations.find(segmentation => segmentation.isActive);
|
||||
}
|
||||
```
|
||||
|
||||
After (3.9)
|
||||
|
||||
```js
|
||||
public getActiveSegmentation(viewportId: string): Segmentation | null {
|
||||
return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId);
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Key Changes</summary>
|
||||
|
||||
1. **Viewport Specificity**
|
||||
- Before: Global active segmentation across all tool groups
|
||||
- After: Active segmentation per viewport
|
||||
2. **Required Parameters**
|
||||
- Before: No parameters needed
|
||||
- After: Requires viewportId parameter
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
**Before:**
|
||||
|
||||
```js
|
||||
// Get active segmentation
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation();
|
||||
if (activeSegmentation) {
|
||||
console.log('Active segmentation:', activeSegmentation.segmentationId);
|
||||
console.log('Active segment:', activeSegmentation.activeSegmentIndex);
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```js
|
||||
// Get active segmentation for specific viewport
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation('viewport1');
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## getToolGroupIdsWithSegmentation
|
||||
|
||||
is now -> `getViewportIdsWithSegmentation` as you guessed
|
||||
|
||||
|
||||
|
||||
## setActiveSegmentationForToolGroup
|
||||
|
||||
-> setActiveSegmentation
|
||||
|
||||
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
setActiveSegmentationForToolGroup(
|
||||
segmentationId: string,
|
||||
toolGroupId?: string,
|
||||
suppressEvents?: boolean
|
||||
): void
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
setActiveSegmentation(
|
||||
viewportId: string,
|
||||
segmentationId: string
|
||||
): void
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
1. **Basic Usage Update**
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.setActiveSegmentationForToolGroup(
|
||||
segmentationId,
|
||||
toolGroupId
|
||||
);
|
||||
// After - OHIF 3.9
|
||||
segmentationService.setActiveSegmentation(
|
||||
viewportId,
|
||||
segmentationId
|
||||
);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## addSegment
|
||||
|
||||
The `addSegment` method in OHIF 3.9 has been updated to handle segmentation properties in a viewport-centric way, removing tool group dependencies and simplifying the configuration structure.
|
||||
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
addSegment(
|
||||
segmentationId: string,
|
||||
config: {
|
||||
segmentIndex?: number;
|
||||
toolGroupId?: string;
|
||||
properties?: {
|
||||
label?: string;
|
||||
color?: ohifTypes.RGB;
|
||||
opacity?: number;
|
||||
visibility?: boolean;
|
||||
isLocked?: boolean;
|
||||
active?: boolean;
|
||||
};
|
||||
}
|
||||
): void
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
addSegment(
|
||||
segmentationId: string,
|
||||
config: {
|
||||
segmentIndex?: number;
|
||||
label?: string;
|
||||
isLocked?: boolean;
|
||||
active?: boolean;
|
||||
color?: csTypes.Color;
|
||||
visibility?: boolean;
|
||||
}
|
||||
): void
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Key Changes</summary>
|
||||
|
||||
1. **Configuration Structure**
|
||||
- Removed double nested `properties` object
|
||||
- Configuration options now at top level
|
||||
- Removed `toolGroupId` parameter
|
||||
- Removed `opacity` parameter (now part of color)
|
||||
2. **Segment Index Generation**
|
||||
- Changed from length-based to max-value-based indexing
|
||||
- More reliable for non-sequential segment indices
|
||||
3. **Color Handling**
|
||||
- Color now includes alpha channel (opacity)
|
||||
- Applied to all relevant viewports automatically
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
1. **Basic Segment Creation**
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
properties: {
|
||||
label: 'Segment 1'
|
||||
}
|
||||
});
|
||||
// After - OHIF 3.9
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
label: 'Segment 1'
|
||||
});
|
||||
```
|
||||
|
||||
2. **Creating Segment with Color**
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
properties: {
|
||||
color: [255, 0, 0],
|
||||
opacity: 255
|
||||
}
|
||||
});
|
||||
// After - OHIF 3.9
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
color: [255, 0, 0, 255] // RGB + Alpha
|
||||
});
|
||||
```
|
||||
|
||||
3. **Setting Visibility and Lock Status**
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
toolGroupId: 'myToolGroup',
|
||||
properties: {
|
||||
visibility: true,
|
||||
isLocked: true
|
||||
}
|
||||
});
|
||||
// After - OHIF 3.9
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
visibility: true,
|
||||
isLocked: true
|
||||
});
|
||||
```
|
||||
|
||||
4. **Complete Configuration Example**
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
segmentIndex: 1,
|
||||
toolGroupId: 'myToolGroup',
|
||||
properties: {
|
||||
label: 'Tumor',
|
||||
color: [255, 0, 0],
|
||||
opacity: 200,
|
||||
visibility: true,
|
||||
isLocked: false,
|
||||
active: true
|
||||
}
|
||||
});
|
||||
// After - OHIF 3.9
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
segmentIndex: 1,
|
||||
label: 'Tumor',
|
||||
color: [255, 0, 0, 200], // RGB + Alpha
|
||||
visibility: true,
|
||||
isLocked: false,
|
||||
active: true
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Important Changes</summary>
|
||||
|
||||
1. **Tool Group Removal**
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
toolGroupId: 'myToolGroup'
|
||||
// ... other properties
|
||||
});
|
||||
// After - OHIF 3.9
|
||||
// No tool group needed - automatically applies to all relevant viewports
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
// ... properties
|
||||
});
|
||||
```
|
||||
|
||||
2. **Segment Index Generation**
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
// Used array length
|
||||
segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length;
|
||||
// After - OHIF 3.9
|
||||
// Uses highest existing index + 1
|
||||
segmentIndex = Math.max(...Object.keys(csSegmentation.segments).map(Number)) + 1;
|
||||
```
|
||||
|
||||
3. **Color and Opacity**
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
properties: {
|
||||
color: [255, 0, 0],
|
||||
opacity: 200
|
||||
}
|
||||
});
|
||||
|
||||
// After - OHIF 3.9
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
color: [255, 0, 0, 200] // Combined color and opacity
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## getActiveSegment
|
||||
|
||||
now requires viewportId, since we have moved away from global active segmentation to viewport specific one
|
||||
|
||||
**API Changes**
|
||||
|
||||
```js
|
||||
// Before
|
||||
getActiveSegment(): Segment
|
||||
|
||||
// After
|
||||
getActiveSegment(viewportId: string): Segment | null
|
||||
```
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
---
|
||||
id: seg-representation
|
||||
title: Segmentation Representations
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Segmentation Representation Management API
|
||||
|
||||
```js
|
||||
addSegmentationRepresentationToToolGroup
|
||||
removeSegmentationRepresentationFromToolGroup
|
||||
getSegmentationRepresentationsForToolGroup
|
||||
```
|
||||
|
||||
In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations.
|
||||
|
||||
|
||||
### Adding Segmentation Representations
|
||||
|
||||
**Before (3.8)**:
|
||||
|
||||
```js
|
||||
// Tool group-based approach
|
||||
await segmentation.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
);
|
||||
```
|
||||
|
||||
**After (3.9)**:
|
||||
|
||||
```js
|
||||
// Viewport-centric approach
|
||||
await segmentation.addSegmentationRepresentation(
|
||||
viewportId,
|
||||
{
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Removing Segmentation Representations
|
||||
|
||||
**Before** :
|
||||
|
||||
```js
|
||||
// Remove specific representations from a tool group
|
||||
segmentation.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId,
|
||||
[segmentationRepresentationUID]
|
||||
);
|
||||
// Remove all representations from a tool group
|
||||
segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
```
|
||||
|
||||
**After**
|
||||
|
||||
```js
|
||||
// Remove specific representation from a viewport
|
||||
segmentation.removeSegmentationRepresentation(
|
||||
viewportId,
|
||||
{
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
}
|
||||
);
|
||||
// Remove all representations from a viewport
|
||||
segmentation.removeSegmentationRepresentations(viewportId);
|
||||
```
|
||||
|
||||
### Getting Segmentation Representations
|
||||
|
||||
**Before**:
|
||||
|
||||
```js
|
||||
// Get representations for a tool group
|
||||
const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId);
|
||||
```
|
||||
|
||||
**After** :
|
||||
|
||||
```js
|
||||
// Get all representations for a viewport
|
||||
const representations = segmentation.getSegmentationRepresentations(viewportId);
|
||||
|
||||
// Get specific type of representations
|
||||
const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
|
||||
// Get representations for specific segmentation
|
||||
const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
segmentationId: segmentationId
|
||||
});
|
||||
|
||||
// Get specific representation
|
||||
const representation = segmentation.getSegmentationRepresentation(viewportId, {
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
```
|
||||
|
||||
### Understanding the Specifier Pattern
|
||||
|
||||
The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include:
|
||||
|
||||
```js
|
||||
type Specifier = {
|
||||
segmentationId?: string; // The ID of the segmentation
|
||||
type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
The specifier pattern allows for:
|
||||
|
||||
1. **Precise Targeting**: You can target specific segmentations and representation types
|
||||
- Allows direct access to individual segmentations
|
||||
- Enables filtering by representation type
|
||||
|
||||
2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation
|
||||
- Query by segmentation ID
|
||||
- Query by representation type
|
||||
- Combine queries for specific needs
|
||||
|
||||
3. **Granular Control**: You can manage representations at different levels of specificity
|
||||
- Viewport level control
|
||||
- Segmentation level control
|
||||
- Individual representation type control
|
||||
|
||||
### Examples of Specifier Usage
|
||||
|
||||
```js
|
||||
// Get all labelmap representations in a viewport
|
||||
const labelmaps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
|
||||
// Get all representations of a specific segmentation (including contour, labelmap, surface)
|
||||
const segReps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
segmentationId: 'seg123'
|
||||
});
|
||||
|
||||
// Get a specific representation
|
||||
const specificRep = segmentation.getSegmentationRepresentation(viewportId, {
|
||||
segmentationId: 'seg123',
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Benefits of the New Approach</summary>
|
||||
|
||||
1. **Direct Viewport Control**:
|
||||
- Each viewport can have its own unique representation configuration
|
||||
- No need to create separate tool groups for different viewport representations
|
||||
2. **Simpler Mental Model**:
|
||||
- Representations are directly tied to where they're displayed
|
||||
- No intermediate tool group layer to manage
|
||||
3. **More Flexible Rendering**:
|
||||
- Each viewport can render the same segmentation differently
|
||||
- Better support for multiple views of the same data
|
||||
4. **Improved Type Safety**:
|
||||
- Specifier pattern provides better TypeScript support
|
||||
- More explicit API with clearer intentions
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Tips</summary>
|
||||
|
||||
1. **Replace Tool Group References**:
|
||||
- Search your codebase for `toolGroupId` references in segmentation code
|
||||
- Replace with appropriate `viewportId` references
|
||||
2. **Update Event Handlers**:
|
||||
- Update any code listening for segmentation events
|
||||
- Events now include viewportId instead of toolGroupId
|
||||
3. **Review Representation Management**:
|
||||
- Identify where you manage segmentation representations
|
||||
- Convert to using the new viewport-centric methods
|
||||
4. **Consider Viewport Context**:
|
||||
- Think about segmentation representation in terms of viewport display
|
||||
- Use specifiers to target specific representations when needed
|
||||
|
||||
</details>
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
---
|
||||
id: seg-creation
|
||||
title: SegmentationService Creation
|
||||
---
|
||||
|
||||
|
||||
|
||||
## createSegmentationForDisplaySet
|
||||
|
||||
is now -> `createLabelmapForDisplaySet`
|
||||
|
||||
Since we are moving towards segmentations be contours as well, this is renamed to clearly state the purpose.
|
||||
Since OHIF 3.9 introduced Stack Segmentation support, we no longer generate a volume-based labelmap or convert the viewport to a volume viewport by default. Our default creation is now stack-based.
|
||||
|
||||
API Changes
|
||||
- `createSegmentationForDisplaySet` has been renamed to `createLabelmapForDisplaySet`.
|
||||
- Pass a `displaySet` object instead of a `displaySetInstanceUID`. This change enhances type safety and flexibility, accommodating future updates to the `displaySetService`.
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
async createSegmentationForDisplaySet(
|
||||
displaySetInstanceUID: string,
|
||||
options?: {
|
||||
segmentationId: string;
|
||||
FrameOfReferenceUID: string;
|
||||
label: string;
|
||||
}
|
||||
): Promise<string>
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
// Method 1: Display Set Based
|
||||
async createLabelmapForDisplaySet(
|
||||
displaySet: DisplaySet,
|
||||
options?: {
|
||||
segmentationId?: string;
|
||||
label: string;
|
||||
segments?: {
|
||||
[segmentIndex: number]: Partial<Segment>
|
||||
};
|
||||
}
|
||||
): Promise<string>
|
||||
```
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
displaySetInstanceUID,
|
||||
{
|
||||
label: 'My Segmentation'
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
```js
|
||||
// After - OHIF 3.9
|
||||
// Option 1: If you have a display set UID
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
const segmentationId = await segmentationService.createLabelmapForDisplaySet(
|
||||
displaySet,
|
||||
{
|
||||
label: 'My Segmentation'
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## createSegmentationForRTDisplaySet
|
||||
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
async createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet,
|
||||
segmentationId?: string,
|
||||
suppressEvents = false
|
||||
): Promise<string>
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
async createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet,
|
||||
options: {
|
||||
segmentationId?: string;
|
||||
type: SegmentationRepresentations; // not required, defaults to Contour
|
||||
}
|
||||
): Promise<string>
|
||||
```
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
if you were not passing segmentationId, you don't need to change anything
|
||||
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet
|
||||
);
|
||||
|
||||
// After - OHIF 3.9
|
||||
const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet,
|
||||
);
|
||||
```
|
||||
|
||||
if you were passing segmentationId, you need to update the API to pass an options object and set the segmentationId in there.
|
||||
|
||||
```js
|
||||
// Before - OHIF 3.8
|
||||
const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet,
|
||||
'custom-id',
|
||||
);
|
||||
// After - OHIF 3.9
|
||||
const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet,
|
||||
{
|
||||
segmentationId: 'custom-id',
|
||||
type: csToolsEnums.SegmentationRepresentations.Contour
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
||||
## createSegmentationForSEGDisplaySet Changes
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
async createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
segmentationId?: string,
|
||||
suppressEvents = false
|
||||
): Promise<string>
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
async createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
options: {
|
||||
segmentationId?: string;
|
||||
type: SegmentationRepresentations; // not required, defaults to Labelmap
|
||||
}
|
||||
): Promise<string>
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
1. **Basic Usage Update**
|
||||
|
||||
```
|
||||
// Before - OHIF 3.8
|
||||
const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet
|
||||
);
|
||||
// After - OHIF 3.9
|
||||
const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
{
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
2. **Custom Configuration**
|
||||
|
||||
```
|
||||
// Before - OHIF 3.8
|
||||
const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
'custom-id',
|
||||
false
|
||||
);
|
||||
// After - OHIF 3.9
|
||||
const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
{
|
||||
segmentationId: 'custom-id',
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
}
|
||||
);
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
---
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
---
|
||||
id: seg-service-mod
|
||||
title: SegmentationService Modifications
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Segmentation Representation Management API
|
||||
|
||||
```js
|
||||
addSegmentationRepresentationToToolGroup
|
||||
removeSegmentationRepresentationFromToolGroup
|
||||
getSegmentationRepresentationsForToolGroup
|
||||
```
|
||||
|
||||
In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations.
|
||||
|
||||
|
||||
### Adding Segmentation Representations
|
||||
|
||||
**Before (3.8)**:
|
||||
|
||||
```js
|
||||
// Tool group-based approach
|
||||
await segmentation.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
);
|
||||
```
|
||||
|
||||
**After (3.9)**:
|
||||
|
||||
```js
|
||||
// Viewport-centric approach
|
||||
await segmentation.addSegmentationRepresentation(
|
||||
viewportId,
|
||||
{
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Removing Segmentation Representations
|
||||
|
||||
**Before** :
|
||||
|
||||
```js
|
||||
// Remove specific representations from a tool group
|
||||
segmentation.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId,
|
||||
[segmentationRepresentationUID]
|
||||
);
|
||||
// Remove all representations from a tool group
|
||||
segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
```
|
||||
|
||||
**After**
|
||||
|
||||
```js
|
||||
// Remove specific representation from a viewport
|
||||
segmentation.removeSegmentationRepresentation(
|
||||
viewportId,
|
||||
{
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
}
|
||||
);
|
||||
// Remove all representations from a viewport
|
||||
segmentation.removeSegmentationRepresentations(viewportId);
|
||||
```
|
||||
|
||||
### Getting Segmentation Representations
|
||||
|
||||
**Before**:
|
||||
|
||||
```js
|
||||
// Get representations for a tool group
|
||||
const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId);
|
||||
```
|
||||
|
||||
**After** :
|
||||
|
||||
```js
|
||||
// Get all representations for a viewport
|
||||
const representations = segmentation.getSegmentationRepresentations(viewportId);
|
||||
|
||||
// Get specific type of representations
|
||||
const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
|
||||
// Get representations for specific segmentation
|
||||
const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
segmentationId: segmentationId
|
||||
});
|
||||
|
||||
// Get specific representation
|
||||
const representation = segmentation.getSegmentationRepresentation(viewportId, {
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
```
|
||||
|
||||
### Understanding the Specifier Pattern
|
||||
|
||||
The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include:
|
||||
|
||||
```js
|
||||
type Specifier = {
|
||||
segmentationId?: string; // The ID of the segmentation
|
||||
type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
The specifier pattern allows for:
|
||||
|
||||
1. **Precise Targeting**: You can target specific segmentations and representation types
|
||||
- Allows direct access to individual segmentations
|
||||
- Enables filtering by representation type
|
||||
|
||||
2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation
|
||||
- Query by segmentation ID
|
||||
- Query by representation type
|
||||
- Combine queries for specific needs
|
||||
|
||||
3. **Granular Control**: You can manage representations at different levels of specificity
|
||||
- Viewport level control
|
||||
- Segmentation level control
|
||||
- Individual representation type control
|
||||
|
||||
### Examples of Specifier Usage
|
||||
|
||||
```js
|
||||
// Get all labelmap representations in a viewport
|
||||
const labelmaps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
|
||||
// Get all representations of a specific segmentation (including contour, labelmap, surface)
|
||||
const segReps = segmentation.getSegmentationRepresentations(viewportId, {
|
||||
segmentationId: 'seg123'
|
||||
});
|
||||
|
||||
// Get a specific representation
|
||||
const specificRep = segmentation.getSegmentationRepresentation(viewportId, {
|
||||
segmentationId: 'seg123',
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Benefits of the New Approach</summary>
|
||||
|
||||
1. **Direct Viewport Control**:
|
||||
- Each viewport can have its own unique representation configuration
|
||||
- No need to create separate tool groups for different viewport representations
|
||||
2. **Simpler Mental Model**:
|
||||
- Representations are directly tied to where they're displayed
|
||||
- No intermediate tool group layer to manage
|
||||
3. **More Flexible Rendering**:
|
||||
- Each viewport can render the same segmentation differently
|
||||
- Better support for multiple views of the same data
|
||||
4. **Improved Type Safety**:
|
||||
- Specifier pattern provides better TypeScript support
|
||||
- More explicit API with clearer intentions
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Tips</summary>
|
||||
|
||||
1. **Replace Tool Group References**:
|
||||
- Search your codebase for `toolGroupId` references in segmentation code
|
||||
- Replace with appropriate `viewportId` references
|
||||
2. **Update Event Handlers**:
|
||||
- Update any code listening for segmentation events
|
||||
- Events now include viewportId instead of toolGroupId
|
||||
3. **Review Representation Management**:
|
||||
- Identify where you manage segmentation representations
|
||||
- Convert to using the new viewport-centric methods
|
||||
4. **Consider Viewport Context**:
|
||||
- Think about segmentation representation in terms of viewport display
|
||||
- Use specifiers to target specific representations when needed
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
---
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
---
|
||||
id: seg-style
|
||||
title: SegmentationService Style
|
||||
---
|
||||
|
||||
|
||||
## Style
|
||||
|
||||
|
||||
### setSegmentVisibility
|
||||
|
||||
since visibility is viewport concern and representation is what is being toggled ->
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
setSegmentVisibility(
|
||||
segmentationId: string,
|
||||
segmentIndex: number,
|
||||
isVisible: boolean,
|
||||
toolGroupId?: string
|
||||
): void
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
setSegmentVisibility(
|
||||
viewportId: string,
|
||||
segmentationId: string,
|
||||
segmentIndex: number,
|
||||
isVisible: boolean,
|
||||
type?: SegmentationRepresentations
|
||||
): void
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Migration Example</summary>
|
||||
|
||||
```js
|
||||
// Before
|
||||
segmentationService.setSegmentVisibility(
|
||||
'segmentation1',
|
||||
1,
|
||||
true,
|
||||
'toolGroup1'
|
||||
);
|
||||
// After
|
||||
segmentationService.setSegmentVisibility(
|
||||
'viewport1',
|
||||
'segmentation1',
|
||||
1,
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
**Getting Viewport IDs**
|
||||
|
||||
When you need to update visibility across multiple viewports:
|
||||
|
||||
```js
|
||||
// Before
|
||||
const toolGroupIds = ['toolGroup1', 'toolGroup2'];
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segmentationService.setSegmentVisibility(
|
||||
'segmentation1',
|
||||
1,
|
||||
true,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
// After
|
||||
const viewportIds = segmentationService.getViewportIdsWithSegmentation('segmentation1');
|
||||
viewportIds.forEach(viewportId => {
|
||||
segmentationService.setSegmentVisibility(
|
||||
viewportId,
|
||||
'segmentation1',
|
||||
1,
|
||||
true
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### get/set Configuration -> get/setStyle
|
||||
|
||||
The segmentation configuration system has been completely redesigned:
|
||||
|
||||
- Moved from global/toolGroup configuration to viewport-specific styles
|
||||
- Split rendering of inactive segmentations into separate API
|
||||
- More granular control over styles at different levels (global, segmentation, viewport, segment)
|
||||
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
interface SegmentationConfig {
|
||||
brushSize: number;
|
||||
brushThresholdGate: number;
|
||||
fillAlpha: number;
|
||||
fillAlphaInactive: number;
|
||||
outlineWidthActive: number;
|
||||
renderFill: boolean;
|
||||
renderInactiveSegmentations: boolean;
|
||||
renderOutline: boolean;
|
||||
outlineOpacity: number;
|
||||
outlineOpacityInactive: number;
|
||||
}
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
// Style Types
|
||||
interface StyleSpecifier {
|
||||
viewportId?: string;
|
||||
segmentationId?: string;
|
||||
type: SegmentationRepresentations;
|
||||
segmentIndex?: number;
|
||||
}
|
||||
interface LabelmapStyle {
|
||||
renderOutline: boolean;
|
||||
outlineWidth: number;
|
||||
renderFill: boolean;
|
||||
fillAlpha: number;
|
||||
outlineAlpha: number;
|
||||
// ....
|
||||
}
|
||||
// Functions
|
||||
getStyle(specifier: StyleSpecifier): LabelmapStyle | ContourStyle | SurfaceStyle;
|
||||
setStyle(specifier: StyleSpecifier, style: LabelmapStyle | ContourStyle | SurfaceStyle): void;
|
||||
setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void;
|
||||
getRenderInactiveSegmentations(viewportId: string): boolean;
|
||||
```
|
||||
|
||||
|
||||
**Before:**
|
||||
|
||||
```js
|
||||
// Get global configuration
|
||||
const config = segmentationService.getConfiguration();
|
||||
console.log(config.fillAlpha, config.renderOutline);
|
||||
// Get tool group specific config
|
||||
const toolGroupConfig = segmentationService.getConfiguration('toolGroup1');
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```js
|
||||
// Get global style for labelmap
|
||||
const labelmapStyle = segmentationService.getStyle({
|
||||
type: SegmentationRepresentations.Labelmap
|
||||
});
|
||||
// Get viewport-specific style
|
||||
const viewportStyle = segmentationService.getStyle({
|
||||
viewportId: 'viewport1',
|
||||
type: SegmentationRepresentations.Labelmap
|
||||
});
|
||||
// Get segmentation-specific style
|
||||
const segmentationStyle = segmentationService.getStyle({
|
||||
segmentationId: 'seg1',
|
||||
type: SegmentationRepresentations.Labelmap
|
||||
});
|
||||
// Get segment-specific style
|
||||
const segmentStyle = segmentationService.getStyle({
|
||||
segmentationId: 'seg1',
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
segmentIndex: 1
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Setting Configuration/Style**
|
||||
|
||||
**Before:**
|
||||
|
||||
```js
|
||||
segmentationService.setConfiguration({
|
||||
fillAlpha: 0.5,
|
||||
outlineWidthActive: 2,
|
||||
renderOutline: true,
|
||||
renderFill: true,
|
||||
renderInactiveSegmentations: true
|
||||
});
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```js
|
||||
// Set global style
|
||||
segmentationService.setStyle(
|
||||
{ type: SegmentationRepresentations.Labelmap },
|
||||
{
|
||||
fillAlpha: 0.5,
|
||||
outlineWidth: 2,
|
||||
renderOutline: true,
|
||||
renderFill: true
|
||||
}
|
||||
);
|
||||
// Set viewport-specific style
|
||||
segmentationService.setStyle(
|
||||
{
|
||||
viewportId: 'viewport1',
|
||||
type: SegmentationRepresentations.Labelmap
|
||||
},
|
||||
{
|
||||
fillAlpha: 0.5,
|
||||
outlineWidth: 2
|
||||
}
|
||||
);
|
||||
// Handle inactive segmentations separately
|
||||
segmentationService.setRenderInactiveSegmentations('viewport1', true);
|
||||
```
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
**Combining Multiple Style Settings**
|
||||
|
||||
**Before:**
|
||||
|
||||
```js
|
||||
segmentationService.setConfiguration({
|
||||
fillAlpha: 0.5,
|
||||
fillAlphaInactive: 0.2,
|
||||
outlineWidthActive: 2,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.5,
|
||||
renderOutline: true,
|
||||
renderFill: true,
|
||||
renderInactiveSegmentations: true
|
||||
});
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```js
|
||||
// Set base style
|
||||
segmentationService.setStyle(
|
||||
{ type: SegmentationRepresentations.Labelmap },
|
||||
{
|
||||
fillAlpha: 0.5,
|
||||
outlineWidth: 2,
|
||||
outlineAlpha: 1,
|
||||
renderOutline: true,
|
||||
renderFill: true
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
**Set inactive rendering per viewport**
|
||||
|
||||
```js
|
||||
segmentationService.setRenderInactiveSegmentations('viewport1', true);
|
||||
// Set style for inactive segments if needed
|
||||
segmentationService.setStyle(
|
||||
{
|
||||
viewportId: 'viewport1',
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
segmentationId: 'seg1'
|
||||
},
|
||||
{
|
||||
fillAlpha: 0.2,
|
||||
outlineAlpha: 0.5
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## setSegmentRGBAColor , setSegmentOpacity, setSegmentRGBA
|
||||
Previously, the SegmentationService had multiple redundant methods for setting colors and opacity (`setSegmentRGBA`, `setSegmentColor`, `setSegmentOpacity`). This led to confusion and potential state inconsistencies between the service and Cornerstone.js Tools.
|
||||
|
||||
The old methods (`setSegmentRGBA`, `setSegmentRGBA`, and `setSegmentOpacity`) are now removed.
|
||||
|
||||
|
||||
1. Replace `setSegmentRGBAColor`, `setSegmentRGBA`, and `setSegmentOpacity` calls: Replace all instances of the old methods with the new `setSegmentColor` method. Note that you now need to provide the `viewportId` as the first argument since segment color is managed per viewport and representation in cornerstone3D.
|
||||
|
||||
|
||||
**Before**
|
||||
|
||||
```js
|
||||
// Old API:
|
||||
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, rgbaColor, toolGroupId);
|
||||
segmentationService.setSegmentRGBA(segmentationId, segmentIndex, rgbaColor, toolGroupId);
|
||||
segmentationService.setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId);
|
||||
```
|
||||
|
||||
**After**
|
||||
|
||||
```js
|
||||
// New API:
|
||||
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); // color is an array of [red, green, blue, alpha]
|
||||
```
|
||||
|
||||
The new `color` argument is an array representing the RGBA color, where the alpha component determines the opacity. Since the Cornerstone Tools library handles segment color per viewport and representation, we require the `viewportId` as an argument now.
|
||||
|
||||
|
||||
|
||||
2. **Retrieve Segment Color using** `getSegmentColor`: The new `getSegmentColor` provides a way to fetch the color of a segment within a specific viewport.
|
||||
|
||||
```js
|
||||
const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex); //returns [r, g, b, a]
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## ToggleSegmentationVisibility
|
||||
|
||||
In Cornerstone3D v2.x, `toggleSegmentationVisibility` has been replaced with `toggleSegmentationRepresentationVisibility`. This change reflects the fact that
|
||||
a representation is what is being toggled, not the segmentation.
|
||||
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
// Toggle visibility for a segmentation globally
|
||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
// Toggle visibility for a segmentation representation in a specific viewport
|
||||
segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
|
||||
segmentationId: segmentationId,
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
});
|
||||
```
|
||||
|
||||
**Migration Steps**
|
||||
|
||||
1. Update all calls to `toggleSegmentationVisibility` to use `toggleSegmentationRepresentationVisibility`
|
||||
2. Add the required `viewportId` parameter
|
||||
3. Add a `type` parameter specifying the representation type (e.g., Labelmap, Contour)
|
||||
4. If you were toggling visibility across all viewports, you'll need to loop through the viewports:
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Additional Notes</summary>
|
||||
|
||||
|
||||
- Each viewport can now have independent visibility settings for the same segmentation
|
||||
- The visibility state is specific to the representation type (Labelmap, Contour, etc.)
|
||||
- To check current visibility, use `getSegmentationRepresentationVisibility(viewportId, { segmentationId, type })`
|
||||
</details>
|
||||
|
||||
---
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
---
|
||||
id: seg-other
|
||||
title: Other Changes
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## addOrUpdateSegmentation
|
||||
|
||||
This was a public method but there is a good chance you were not using it
|
||||
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
// Before
|
||||
addOrUpdateSegmentation(
|
||||
segmentation: Segmentation,
|
||||
suppressEvents = false,
|
||||
notYetUpdatedAtSource = false
|
||||
): string
|
||||
```
|
||||
|
||||
**After**
|
||||
|
||||
```js
|
||||
addOrUpdateSegmentation(
|
||||
segmentationInput: SegmentationPublicInput | Partial<Segmentation>
|
||||
)
|
||||
```
|
||||
|
||||
### Data Structure Changes
|
||||
|
||||
The segmentation object that was used previously was a custom segmentation object that was used internally by the SegmentationService. But
|
||||
we have moved to the cornerstone public segmentation input type.
|
||||
|
||||
**Before:**
|
||||
|
||||
```js
|
||||
const segmentation = {
|
||||
id: 'segmentation1',
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
isActive: true,
|
||||
activeSegmentIndex: 1,
|
||||
segments: [
|
||||
{
|
||||
segmentIndex: 1,
|
||||
color: [255, 0, 0],
|
||||
isVisible: true,
|
||||
isLocked: false,
|
||||
opacity: 255
|
||||
}
|
||||
],
|
||||
label: 'Segmentation 1',
|
||||
cachedStats: {},
|
||||
representationData: {
|
||||
LABELMAP: {
|
||||
volumeId: 'volume1',
|
||||
referencedVolumeId: 'reference1'
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
**After:**
|
||||
|
||||
This matches the cornerstone public segmentation input type.
|
||||
|
||||
```js
|
||||
const segmentationInput = {
|
||||
segmentationId: 'segmentation1',
|
||||
representation: {
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
data: {
|
||||
imageIds: segmentationImageIds,
|
||||
referencedVolumeId: 'reference1'
|
||||
}
|
||||
},
|
||||
config: {
|
||||
label: 'Segmentation 1',
|
||||
segments: {
|
||||
1: {
|
||||
label: 'Segment 1',
|
||||
active: true,
|
||||
locked: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
|
||||
```js
|
||||
// Before
|
||||
const newSegmentation = {
|
||||
id: 'seg1',
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
segments: [...],
|
||||
representationData: {
|
||||
LABELMAP: {
|
||||
volumeId: 'volume1',
|
||||
referencedVolumeId: 'reference1'
|
||||
}
|
||||
}
|
||||
};
|
||||
segmentationService.addOrUpdateSegmentation(newSegmentation);
|
||||
|
||||
// After
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
segmentationId: 'seg1',
|
||||
representation: {
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
data: {
|
||||
imageIds: segmentationImageIds,
|
||||
referencedVolumeId: 'reference1'
|
||||
}
|
||||
},
|
||||
config: {
|
||||
segments: {
|
||||
1: {
|
||||
label: 'Segment 1',
|
||||
active: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
**Updating Existing Segmentation**
|
||||
|
||||
```js
|
||||
// Before
|
||||
const updatedSegmentation = {
|
||||
...existingSegmentation,
|
||||
segments: [...modifiedSegments],
|
||||
activeSegmentIndex: 2
|
||||
};
|
||||
segmentationService.addOrUpdateSegmentation(updatedSegmentation);
|
||||
|
||||
// After
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
segmentationId: 'seg1',
|
||||
config: {
|
||||
segments: {
|
||||
2: { active: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## loadSegmentationsForViewport
|
||||
|
||||
same as addOrUpdateSegmentation, you should pass in the new segmentation data structure.
|
||||
|
||||
For instance
|
||||
|
||||
**Before**
|
||||
|
||||
```js
|
||||
const segmentations = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Segmentations',
|
||||
segments: labels.map((label, index) => ({
|
||||
segmentIndex: index + 1,
|
||||
label
|
||||
})),
|
||||
isActive: true,
|
||||
activeSegmentIndex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
commandsManager.runCommand('loadSegmentationsForViewport', {
|
||||
segmentations,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
**After**
|
||||
|
||||
```js
|
||||
|
||||
const labels = ['Segment 1', 'Segment 2', 'Segment 3'];
|
||||
|
||||
const segmentations = [
|
||||
{
|
||||
segmentationId: '1',
|
||||
representation: {
|
||||
type: Enums.SegmentationRepresentations.Labelmap,
|
||||
},
|
||||
config: {
|
||||
label: 'Segmentations',
|
||||
segments: labels.reduce((acc, label, index) => {
|
||||
acc[index + 1] = {
|
||||
label,
|
||||
active: index === 0, // First segment is active
|
||||
locked: false,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
commandsManager.runCommand('loadSegmentationsForViewport', {
|
||||
segmentations,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## highlightSegment
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
// Before (v1.x)
|
||||
highlightSegment(
|
||||
segmentationId: string,
|
||||
segmentIndex: number,
|
||||
toolGroupId?: string,
|
||||
alpha = 0.9,
|
||||
animationLength = 750,
|
||||
hideOthers = true,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
highlightSegment(
|
||||
segmentationId: string,
|
||||
segmentIndex: number,
|
||||
viewportId?: string, // notice viewportId instead of toolGroupId
|
||||
alpha = 0.9,
|
||||
animationLength = 750,
|
||||
hideOthers = true,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
)
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Key Changes</summary>
|
||||
|
||||
1. Removed `toolGroupId` in favor of `viewportId`
|
||||
2. If no viewportId is provided, highlights in all relevant viewports
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Migration Examples</summary>
|
||||
|
||||
**Basic Usage**
|
||||
|
||||
```js
|
||||
// Before
|
||||
segmentationService.highlightSegment(
|
||||
'seg1',
|
||||
1,
|
||||
'toolGroup1',
|
||||
0.9,
|
||||
750,
|
||||
true,
|
||||
);
|
||||
// After
|
||||
segmentationService.highlightSegment(
|
||||
'seg1',
|
||||
1,
|
||||
'viewport1',
|
||||
0.9,
|
||||
750,
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
**Highlighting in Multiple Views**
|
||||
|
||||
```js
|
||||
// Before
|
||||
const toolGroupIds = ['toolGroup1', 'toolGroup2'];
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segmentationService.highlightSegment(
|
||||
'seg1',
|
||||
1,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
// After - Method 1: Let service handle multiple viewports
|
||||
segmentationService.highlightSegment('seg1', 1);
|
||||
// After - Method 2: Explicitly specify viewports
|
||||
const viewportIds = ['viewport1', 'viewport2'];
|
||||
viewportIds.forEach(viewportId => {
|
||||
segmentationService.highlightSegment(
|
||||
'seg1',
|
||||
1,
|
||||
viewportId
|
||||
);
|
||||
});
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## jumpToSegmentCenter
|
||||
|
||||
**Before (OHIF 3.8)**
|
||||
|
||||
```js
|
||||
jumpToSegmentCenter(
|
||||
segmentationId: string,
|
||||
segmentIndex: number,
|
||||
toolGroupId?: string,
|
||||
highlightAlpha = 0.9,
|
||||
highlightSegment = true,
|
||||
animationLength = 750,
|
||||
highlightHideOthers = false,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
)
|
||||
```
|
||||
|
||||
**After (OHIF 3.9)**
|
||||
|
||||
```js
|
||||
jumpToSegmentCenter(
|
||||
segmentationId: string,
|
||||
segmentIndex: number,
|
||||
viewportId? string, // notice viewportId instead of toolGroupId
|
||||
highlightAlpha = 0.9,
|
||||
highlightSegment = true,
|
||||
animationLength = 750,
|
||||
highlightHideOthers = false,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
)
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Key Changes</summary>
|
||||
|
||||
1. Removed `toolGroupId` parameter infavor of viewportId
|
||||
2. Automatically handles relevant viewports if `viewportId` not provided
|
||||
|
||||
|
||||
```
|
||||
// Before
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
'seg1',
|
||||
1,
|
||||
'toolGroup1'
|
||||
);
|
||||
// After
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
'seg1',
|
||||
1,
|
||||
'viewportId1'
|
||||
);
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
id: segmentation-index
|
||||
title: Segmentation
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
:::info
|
||||
This migration involves significant architectural changes to the segmentation system. While we typically aim for incremental updates, the shift from a tool group-centric to a viewport-centric architecture was necessary to support OHIF 3.9's advanced visualization capabilities, and more flexible segmentation handling.
|
||||
|
||||
Don't worry - we'll guide you through each change step by step!
|
||||
:::
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
id: 2-renamings
|
||||
title: Renamings
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
|
||||
## Panel Measurements
|
||||
|
||||
The panel in the default extension is renamed from `measure` to `measurements` to be more consistent with the rest of the extensions.
|
||||
|
||||
**Action Needed**
|
||||
|
||||
Update any references to the `measure` panel to `measurements` in your code.
|
||||
|
||||
Find and replace
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before 🕰️" default>
|
||||
@ohif/extension-default.panelModule.measure
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After 🚀" >
|
||||
@ohif/extension-default.panelModule.measurements
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: 3-data-sources
|
||||
title: Data Sources
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## BulkDataURI Configuration
|
||||
|
||||
We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one.
|
||||
|
||||
### What's Changing?
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before 🕰️" default>
|
||||
|
||||
```javascript
|
||||
useBulkDataURI: false,
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After 🚀">
|
||||
|
||||
```javascript
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
// Additional configuration **options**
|
||||
},
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
**Additional Notes:**
|
||||
- The new configuration allows for more granular control over BulkDataURI behavior.
|
||||
- You can now add custom URL prefixing logic using the startsWith and prefixWith properties.
|
||||
- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Measurements
|
||||
---
|
||||
|
||||
|
||||
## Display Text
|
||||
|
||||
|
||||
Previously, `displayText` for measurements was often a simple string or an array of strings. This approach made it difficult to distinguish between primary measurement values (e.g., length, area) and secondary information (e.g., series number, instance number). It also limited styling options for differentiating these types of information.
|
||||
|
||||
The new approach introduces a structured object for `displayText`, consisting of `primary` and `secondary` arrays. This separation allows for better organization and presentation of measurement information. The `primary` array is intended for the main measurement values (on the left), while the `secondary` array is for contextual information like series and instance numbers (on the right)
|
||||
|
||||
### Migration Steps
|
||||
|
||||
If you have custom measurement tools or modify existing ones, you need to update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format.
|
||||
|
||||
**Update Measurement Mappings:** If your extension defines custom measurement tools or modifies existing ones, update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format.
|
||||
|
||||
```js
|
||||
// Old Implementation (example for Length tool)
|
||||
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// ...
|
||||
return `${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`;
|
||||
}
|
||||
// New Implementation
|
||||
function getDisplayText(mappedAnnotations, displaySet) {
|
||||
// ...
|
||||
return {
|
||||
primary: [`${roundedLength} ${unit}`], // Primary measurement value
|
||||
secondary: [`S: ${SeriesNumber}${instanceText}${frameText}`], // Secondary information
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### selected property
|
||||
|
||||
`selected` property on measurements is now renamed to `isSelected` to match the rest of `isLocked` , `isVisible` naming convention.
|
||||
|
||||
Migration: you probably don't need to perform any migration
|
||||
|
||||
---
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: viewport-action-corner
|
||||
title: ViewportActionCorner
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Key Changes and Rationale
|
||||
|
||||
Previously, the `ViewportActionCornersService` used the `setComponent` or `setComponents` methods to add components to viewport corners. These methods, when used with multiple components, would essentially overwrite existing components at the same location, unless great care was taken with the `indexPriority` property. This made it difficult to reliably position multiple components within the same corner.
|
||||
|
||||
The new approach introduces the methods `addComponent` and `addComponents`, which insert components into the viewport corners based on an optional `indexPriority` property and provide predictable ordering based on the relative `indexPriority` of the components already at the corner. If no `indexPriority` is given, components are added to the end (for the left side) or the beginning (for the right side) by default.
|
||||
|
||||
### Migration Steps
|
||||
|
||||
**Update Component Addition Methods:** Replace calls to `setComponent` and `setComponents` with `addComponent` and `addComponents`, respectively.
|
||||
|
||||
```js
|
||||
// Old API
|
||||
viewportActionCornersService.setComponent({
|
||||
viewportId,
|
||||
id: 'myComponent',
|
||||
component: <MyComponent />,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight
|
||||
});
|
||||
```
|
||||
|
||||
**New API**
|
||||
|
||||
```js
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: 'myComponent',
|
||||
component: <MyComponent />,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
indexPriority: 1, // indexPriority is now optional and determines placement order within the corner
|
||||
});
|
||||
|
||||
```
|
||||
@@ -0,0 +1,343 @@
|
||||
---
|
||||
id: state-sync-service
|
||||
title: StateSyncService
|
||||
---
|
||||
|
||||
|
||||
## Migrating from StateSyncService to Zustand Stores
|
||||
|
||||
The `StateSyncService` has been deprecated in favor of more modern and efficient state management using Zustand stores. This migration guide outlines the reasons for the change and provides step-by-step instructions on how to migrate your extension or mode from using `StateSyncService` to Zustand.
|
||||
|
||||
## Why Migrate?
|
||||
|
||||
The `StateSyncService` had limitations:
|
||||
|
||||
- **Limited Reactivity:** Updates weren't always reactive, requiring manual re-renders.
|
||||
- **Lack of Granularity:** It stored large chunks of state, hindering performance.
|
||||
- **Complexity:** Managing and syncing state across components was cumbersome.
|
||||
|
||||
Zustand offers several advantages:
|
||||
|
||||
- **Lightweight and Fast:** Zustand is a minimal and performant state management library.
|
||||
- **Granular Control:** Create individual stores for specific data, improving reactivity and performance.
|
||||
- **Simplified API:** Easy-to-use hooks for subscribing and updating state.
|
||||
|
||||
## Migration Steps:
|
||||
|
||||
1. **Identify State to Migrate:** Determine which parts of your extension or mode rely on the `StateSyncService`. Typical examples include:
|
||||
- **Viewport Presentations:** LUT and position information for viewports.
|
||||
- **Layout State:** Custom grid layouts and one-up toggling.
|
||||
- **Synchronizers:** State for cross-viewport synchronization.
|
||||
- **UI State:** UI-specific settings.
|
||||
2. **Replace StateSyncService Usage:** In your extension or mode:
|
||||
- **Import Zustand Stores:** Import the new stores you created.
|
||||
- **Replace** `getState()` and `store()`: Use the Zustand hooks (`useStore`, `set`, `get`) to access and update state in your components.
|
||||
- **Handle Presentation IDs:** Implement logic for generating and managing presentation IDs within your stores or relevant components. This can involve using unique keys based on viewport options, display sets, and unique indices. See the `presentationUtils.ts` file for example implementations.
|
||||
- **Rehydrate State:** On mode entry, rehydrate your Zustand stores with any relevant persisted state from localStorage or other storage mechanisms.
|
||||
- **Clear State on Mode Exit:** Ensure you clear your Zustand stores appropriately on mode exit to prevent memory leaks.
|
||||
|
||||
|
||||
|
||||
### `LutPresentationStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const lutPresentationStore = stateSyncService.getState().lutPresentationStore;
|
||||
const lutPresentation = lutPresentationStore[presentationId];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
lutPresentationStore: {
|
||||
...lutPresentationStore,
|
||||
[presentationId]: newLutPresentation,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useLutPresentationStore } from '../stores/useLutPresentationStore';
|
||||
const { lutPresentationStore, setLutPresentation } = useLutPresentationStore();
|
||||
const lutPresentation = lutPresentationStore[presentationId];
|
||||
// ...to update
|
||||
setLutPresentation(presentationId, newLutPresentation);
|
||||
```
|
||||
|
||||
The `getPresentationId` for `lutPresentationStore` was previously registered in `platform/core`. Now, the Zustand store provides this functionality.
|
||||
|
||||
```js
|
||||
// Fetch getPresentationId functions from respective Zustand stores
|
||||
const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
|
||||
|
||||
// Register presentation id providers
|
||||
viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
### `PositionPresentationStore`
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const positionPresentationStore = stateSyncService.getState().positionPresentationStore;
|
||||
const positionPresentation = positionPresentationStore[presentationId];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
positionPresentationStore: {
|
||||
...positionPresentationStore,
|
||||
[presentationId]: newPositionPresentation,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { usePositionPresentationStore } from '../stores/usePositionPresentationStore';
|
||||
const { positionPresentationStore, setPositionPresentation } = usePositionPresentationStore();
|
||||
const positionPresentation = positionPresentationStore[presentationId];
|
||||
// ...to update
|
||||
setPositionPresentation(presentationId, newPositionPresentation);
|
||||
```
|
||||
|
||||
Similar to lutPresentationId, the PositionPresentationId is also registered from outside
|
||||
|
||||
```js
|
||||
|
||||
const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
|
||||
|
||||
// register presentation id providers
|
||||
viewportGridService.addPresentationIdProvider(
|
||||
'positionPresentationId',
|
||||
getPositionPresentationId
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ViewportGridStore`
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const viewportGridStore = stateSyncService.getState().viewportGridStore;
|
||||
const gridState = viewportGridStore[storeId];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
viewportGridStore: {
|
||||
...viewportGridStore,
|
||||
[storeId]: newGridState,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useViewportGridStore } from '../stores/useViewportGridStore';
|
||||
const { viewportGridState, setViewportGridState } = useViewportGridStore();
|
||||
const gridState = viewportGridState[storeId];
|
||||
// ...to update
|
||||
setViewportGridState(storeId, newGridState);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `DisplaySetSelectorStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const displaySetSelectorMap = stateSyncService.getState().displaySetSelectorMap;
|
||||
const displaySetUID = displaySetSelectorMap[selectorKey];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
displaySetSelectorMap: {
|
||||
...displaySetSelectorMap,
|
||||
[selectorKey]: newDisplaySetUID,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore';
|
||||
const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore();
|
||||
const displaySetUID = displaySetSelectorMap[selectorKey];
|
||||
// ...to update
|
||||
setDisplaySetSelector(selectorKey, newDisplaySetUID);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `HangingProtocolStageIndexStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const hangingProtocolStageIndexMap = stateSyncService.getState().hangingProtocolStageIndexMap;
|
||||
const hpInfo = hangingProtocolStageIndexMap[cacheId];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
hangingProtocolStageIndexMap: {
|
||||
...hangingProtocolStageIndexMap,
|
||||
[cacheId]: newHpInfo,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore';
|
||||
const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } = useHangingProtocolStageIndexStore();
|
||||
const hpInfo = hangingProtocolStageIndexMap[cacheId];
|
||||
// ...to update
|
||||
setHangingProtocolStageIndex(cacheId, newHpInfo);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ToggleHangingProtocolStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const toggleHangingProtocol = stateSyncService.getState().toggleHangingProtocol;
|
||||
const previousHpInfo = toggleHangingProtocol[storedHanging];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
toggleHangingProtocol: {
|
||||
...toggleHangingProtocol,
|
||||
[storedHanging]: newHpInfo,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useToggleHangingProtocolStore } from '../stores/useToggleHangingProtocolStore';
|
||||
const { toggleHangingProtocol, setToggleHangingProtocol } = useToggleHangingProtocolStore();
|
||||
const previousHpInfo = toggleHangingProtocol[storedHanging];
|
||||
// ...to update
|
||||
setToggleHangingProtocol(storedHanging, newHpInfo);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ToggleOneUpViewportGridStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const toggleOneUpViewportGridStore = stateSyncService.getState().toggleOneUpViewportGridStore;
|
||||
const previousGridState = toggleOneUpViewportGridStore.layout; // Assuming layout was a property
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
toggleOneUpViewportGridStore: newGridState,
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useToggleOneUpViewportGridStore } from '../stores/useToggleOneUpViewportGridStore';
|
||||
const { toggleOneUpViewportGridStore, setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore();
|
||||
const previousGridState = toggleOneUpViewportGridStore; // No nested layout property
|
||||
// ...to update
|
||||
setToggleOneUpViewportGridStore(newGridState);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `UIStateStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const uiState = stateSyncService.getState().uiStateStore[someUIKey];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
uiStateStore: {
|
||||
...stateSyncService.getState().uiStateStore,
|
||||
[someUIKey]: newUIState,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useUIStateStore } from '../stores/useUIStateStore';
|
||||
const { uiState, setUIState } = useUIStateStore();
|
||||
const currentUIState = uiState[someUIKey];
|
||||
// ...to update
|
||||
setUIState(someUIKey, newUIState);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ViewportsByPositionStore`
|
||||
|
||||
|
||||
**Before (StateSyncService):**
|
||||
|
||||
```js
|
||||
const stateSyncService = servicesManager.services.stateSyncService;
|
||||
const viewportsByPosition = stateSyncService.getState().viewportsByPosition;
|
||||
const cachedViewport = viewportsByPosition[positionId];
|
||||
// ...to update
|
||||
stateSyncService.store({
|
||||
viewportsByPosition: {
|
||||
...viewportsByPosition,
|
||||
[positionId]: newViewport,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useViewportsByPositionStore } from '../stores/useViewportsByPositionStore';
|
||||
const { viewportsByPosition, setViewportsByPosition } = useViewportsByPositionStore();
|
||||
const cachedViewport = viewportsByPosition[positionId];
|
||||
// ...to update
|
||||
setViewportsByPosition(positionId, newViewport);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `SegmentationPresentationStore`
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```js
|
||||
import { useSegmentationPresentationStore } from '../stores/useSegmentationPresentationStore';
|
||||
const { segmentationPresentationStore, setSegmentationPresentation } =
|
||||
useSegmentationPresentationStore();
|
||||
// ...to update
|
||||
setSegmentationPresentation(presentationId, newSegmentationPresentation);
|
||||
// You likely have functions within the store like:
|
||||
// addSegmentationPresentation
|
||||
// setSegmentationVisibility
|
||||
// etc.
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
id: 6-rtstruct
|
||||
title: RTSTRUCT
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
|
||||
|
||||
# RTStructure Set has transitioned from VTK actors to SVG.
|
||||
|
||||
We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering.
|
||||
|
||||
As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports.
|
||||
|
||||
|
||||
Read more in Pull Requests:
|
||||
- https://github.com/OHIF/Viewers/pull/4074
|
||||
- https://github.com/OHIF/Viewers/pull/4157
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: UI
|
||||
---
|
||||
|
||||
|
||||
## `UINotificationService`
|
||||
|
||||
|
||||
We've switched our custom notification service to the Sonner component from https://sonner.emilkowal.ski/
|
||||
|
||||
### 1. Toast Positions (Kebab-Case)
|
||||
|
||||
Toast positions are now defined using kebab-case instead of camelCase. For instance, `topRight` becomes `top-right`, `bottomRight` becomes `bottom-right`, etc. Ensure your position strings are updated accordingly.
|
||||
|
||||
**Old API:**
|
||||
|
||||
```js
|
||||
uiNotificationService.show({
|
||||
title: 'My Title',
|
||||
message: 'My Message',
|
||||
duration: 3000,
|
||||
position: 'topRight',
|
||||
type: 'error',
|
||||
autoClose: true,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
**New API:**
|
||||
|
||||
```js
|
||||
uiNotificationService.show({
|
||||
title: 'My Title',
|
||||
message: 'My Message',
|
||||
duration: 3000,
|
||||
position: 'top-right', // Note the change to kebab-case
|
||||
type: 'error',
|
||||
autoClose: true,
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Promise Support
|
||||
|
||||
The `show()` method now supports promises, enabling you to display loading notifications and automatically update them based on the promise's resolution or rejection. This significantly simplifies asynchronous operation feedback.
|
||||
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
const myPromise = someAsyncOperation();
|
||||
const notificationId = uiNotificationService.show({
|
||||
title: 'Loading Data',
|
||||
message: 'Fetching data from server...',
|
||||
type: 'info',
|
||||
promise: myPromise,
|
||||
promiseMessages: {
|
||||
loading: 'Fetching...',
|
||||
success: (data) => `Data loaded: ${data.length} items`, // Access promise result
|
||||
error: (error) => `Failed to load data: ${error.message}`, // Access error details
|
||||
},
|
||||
});
|
||||
// Optionally hide notification manually if needed
|
||||
// myPromise.finally(() => uiNotificationService.hide(notificationId));
|
||||
```
|
||||
|
||||
### 3. `hide()` API Change
|
||||
|
||||
The `hide()` method no longer takes an options object. It only accepts the notification ID as a string argument.
|
||||
|
||||
**Old API:**
|
||||
|
||||
```js
|
||||
uiNotificationService.hide({ id: notificationId });
|
||||
```
|
||||
|
||||
**New API:**
|
||||
|
||||
```js
|
||||
uiNotificationService.hide(notificationId);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Viewport Pane Tailwindcss class
|
||||
|
||||
Previously, when targeting the viewport pane to add custom CSS, you likely used `group-hover:visible` with the viewportPane having a `group` class.
|
||||
|
||||
The naming was confusing as we added more groups, so we renamed it to `group/pane`. Now you can apply `group-hover/pane` for better clarity.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Header Component
|
||||
|
||||
|
||||
Header Component has been refactored in the @ohif/ui-next package.
|
||||
|
||||
|
||||
**Before**
|
||||
|
||||
|
||||
```js
|
||||
function Header({
|
||||
children,
|
||||
menuOptions,
|
||||
isReturnEnabled,
|
||||
onClickReturnButton,
|
||||
isSticky,
|
||||
WhiteLabeling,
|
||||
showPatientInfo,
|
||||
servicesManager,
|
||||
Secondary,
|
||||
appConfig,
|
||||
...props
|
||||
}: withAppTypes): ReactNode
|
||||
```
|
||||
|
||||
**After**
|
||||
|
||||
```js
|
||||
function Header({
|
||||
children,
|
||||
menuOptions,
|
||||
isReturnEnabled,
|
||||
onClickReturnButton,
|
||||
isSticky,
|
||||
WhiteLabeling,
|
||||
PatientInfo,
|
||||
Secondary,
|
||||
...props
|
||||
}: HeaderProps): ReactNode
|
||||
```
|
||||
|
||||
The `PatientInfo` component is now preferred, and the `showPatientInfo` prop has been removed. The previous method depended on `servicesManager`, which was cumbersome because the UI shouldn't need to interact with `servicesManager`.
|
||||
|
||||
All the DropDown and Icons are now in the @ohif/ui-next package.
|
||||
|
||||
|
||||
---
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Refactoring
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Panel Segmentation
|
||||
|
||||
is now moved from `@ohif/extension-cornerstone-dicom-seg` to `@ohif/extension-cornerstone`.
|
||||
|
||||
|
||||
The cornerstone extension now provides the panelSegmentation feature, which was previously part of the cornerstone-dicom-seg extension. This change is logical as panelSegmentation handles more than just DICOM. It can process various formats, including custom formats from the backend and potentially NIFTI format in the future.
|
||||
|
||||
|
||||
Before in your modes you were using
|
||||
|
||||
```js
|
||||
'@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
||||
```
|
||||
|
||||
|
||||
Now you should use it via
|
||||
|
||||
|
||||
```js
|
||||
'@ohif/extension-cornerstone.panelModule.panelSegmentation',
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `callInputDialog` and `colorPickerDialog` and `showLabelAnnotationPopup`
|
||||
|
||||
Due to the excessive number of `callInputDialog` instances, we centralized them. You can now import them from `@ohif/extension-default`.
|
||||
|
||||
|
||||
```js
|
||||
import { showLabelAnnotationPopup, callInputDialog, colorPickerDialog } from '@ohif/extension-default';
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## disableEditing
|
||||
|
||||
The configuration has moved from appConfig to allow more precise control over component disabling. To disable editing for segmentation and measurements, add the following settings:
|
||||
|
||||
|
||||
**Before: **
|
||||
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'segmentation.panel',
|
||||
disableEditing: true,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
**Now **
|
||||
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
// To disable editing in the SegmentationTable
|
||||
{
|
||||
id: 'PanelSegmentation.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
// To disable editing in the MeasurementTable
|
||||
{
|
||||
id: 'PanelMeasurement.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Customization Ids
|
||||
|
||||
The primary reason for this migration is to improve modularity and maintainability in configuration management, as we plan to focus more on the customization service in the near future.
|
||||
|
||||
**Before**
|
||||
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'segmentation.panel',
|
||||
segmentationPanelMode: 'expanded',
|
||||
addSegment: false,
|
||||
onSegmentationAdd: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
|
||||
**Now**
|
||||
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Other Changes
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
|
||||
## External Libraries
|
||||
Some libraries are loaded via dynamic import. You can provide a global function
|
||||
`browserImport` the allows loading of dynamic imports without affecting the
|
||||
webpack build. This import looks like:
|
||||
|
||||
```html
|
||||
<script>
|
||||
function browserImportFunction(moduleId) {
|
||||
return import(moduleId);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
and belongs in the root html file for your application.
|
||||
You then need to remove `dependencies` on the external import, and add a reference
|
||||
to the external import in your `pluginConfig.json` file.
|
||||
|
||||
### Example plugin config for `dicom-microscopy-viewer`
|
||||
The example below imports the `dicom-microscopy-viewer` for use as an external
|
||||
dependency. The example is part of the default `pluginConfig.json` file.
|
||||
|
||||
```json
|
||||
"public": [
|
||||
{
|
||||
"directory": "./platform/public"
|
||||
},
|
||||
{
|
||||
"packageName": "dicom-microscopy-viewer",
|
||||
"importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js",
|
||||
"globalName": "dicomMicroscopyViewer",
|
||||
"directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
This defines two directory modules, whose contents are copied unchanged to the
|
||||
output build directory. It then defines the `dicom-microscopy-viewer` using
|
||||
the `packageName` element as being a module which is imported dynamically.
|
||||
Then, the import path passed into the browserImportFunction above is
|
||||
specified, and then how to access the import itself, via the `window.dicomMicroscopyViewer`
|
||||
global name reference.
|
||||
|
||||
### Referencing External Imports
|
||||
The appConfig either defines or has a default peerImport function which can be
|
||||
used to load references to the modules defined in the pluginConfig file. See
|
||||
the example in `init.tsx` for the cornerstone extension for how this is passed
|
||||
into CS3D for loading the whole slide imaging library.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Use of ViewReference for navigation
|
||||
When navigating to measurements and storing/remembering navigation positions,
|
||||
the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable`
|
||||
used to check if a reference can be applied, and finally `viewport.setViewReference` to
|
||||
navigate to a view. Note that this changes the behaviour of navigation between
|
||||
MPR and Stack viewports, and also enables navigation of video and microscopy
|
||||
viewports in CS3D. This can cause some unexpected behaviour depending on how the
|
||||
frame of reference values are configured to allow for navigation.
|
||||
|
||||
The isReferenceViewable is used to determine when a view or measurement can be
|
||||
shown on a given view. For stack versus volume viewports, this can cause unexpected
|
||||
behaviour to be seen depending on how the view reference was fetched.
|
||||
|
||||
### `getViewReference` with `forFrameOfReference`
|
||||
When a view reference is fetched with the for frame of reference flag set to true,
|
||||
a reference will be returned which can be displayed on any viewport containing
|
||||
the same frame of reference and encompassing the given FOR and able to display the required
|
||||
orientation. Without this flag, a view reference is returned which will be
|
||||
displayed on a stack with the given image id, or a volume containing said image id
|
||||
or the specified volume.
|
||||
|
||||
### `isReferenceViewable` with navigation and/or orientation
|
||||
The is reference viewable will return false unless the given reference is directly
|
||||
viewable in the viewport as is. However, it can be passed various flags to determine
|
||||
whether the reference could be displayed if the viewport was modified in various ways,
|
||||
for example, by changing the position or orientation of the viewport. This allows
|
||||
checking for degrees of closeness so that the correct viewport can be chosen.
|
||||
|
||||
Note that this may result in displaying a measurement from one viewport on a completely
|
||||
different viewport, for example, showing a Probe tool from the stack viewport on
|
||||
an MPR view.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
id: 3p8-to-3p9
|
||||
title: 3.8 -> 3.9
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
Here are the changes you need to make to migrate from 3.8 to 3.9.
|
||||
@@ -1,309 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: 3.8 -> 3.9-beta
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
|
||||
# Migration Guide
|
||||
|
||||
|
||||
## React 18 Migration Guide
|
||||
As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version.
|
||||
What's Changing?
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before" default>
|
||||
|
||||
```md
|
||||
- React 17
|
||||
- Using `defaultProps`
|
||||
- `babel-inline-svg` for SVG imports
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After">
|
||||
|
||||
```md
|
||||
- React 18
|
||||
- Default parameters for props
|
||||
- `svgr` for SVG imports
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
### Run newer yarn version
|
||||
You must be running a newer yarn version for react 18.
|
||||
It isn't clear the exact yarn required.
|
||||
|
||||
### Update React version:
|
||||
In your custom extensions and modes, change the version of react and react-dom to ^18.3.1.
|
||||
|
||||
### Replace defaultProps with default parameters:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before" default>
|
||||
|
||||
```jsx
|
||||
const MyComponent = ({ prop1, prop2 }) => {
|
||||
return <div>{prop1} {prop2}</div>
|
||||
}
|
||||
|
||||
MyComponent.defaultProps = {
|
||||
prop1: 'default value',
|
||||
prop2: 'default value'
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After">
|
||||
|
||||
```jsx
|
||||
const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => {
|
||||
return <div>{prop1} {prop2}</div>
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Update SVG imports:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before" default>
|
||||
|
||||
```javascript
|
||||
import arrowDown from './../../assets/icons/arrow-down.svg';
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After">
|
||||
|
||||
```javascript
|
||||
import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg';
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
## Renaming
|
||||
|
||||
The panel in the default extension is renamed from `measure` to `measurements` to be more consistent with the rest of the extensions.
|
||||
|
||||
**Action Needed**
|
||||
|
||||
Update any references to the `measure` panel to `measurements` in your code.
|
||||
|
||||
Find and replace
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before 🕰️" default>
|
||||
@ohif/extension-default.panelModule.measure
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After 🚀" >
|
||||
@ohif/extension-default.panelModule.measurements
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
## RTStructure Set has transitioned from VTK actors to SVG.
|
||||
|
||||
We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering.
|
||||
|
||||
As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports.
|
||||
|
||||
|
||||
Read more in Pull Requests:
|
||||
- https://github.com/OHIF/Viewers/pull/4074
|
||||
- https://github.com/OHIF/Viewers/pull/4157
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
## Crosshairs
|
||||
|
||||
They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off.
|
||||
|
||||
To disable it, remove the configuration from the `initToolGroups` in your mode.
|
||||
|
||||
```
|
||||
{
|
||||
configuration: {
|
||||
viewportIndicators: true,
|
||||
viewportIndicatorsConfig: {
|
||||
circleRadius: 5,
|
||||
xOffset: 0.95,
|
||||
yOffset: 0.05,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
## External Libraries
|
||||
Some libraries are loaded via dynamic import. You can provide a global function
|
||||
`browserImport` the allows loading of dynamic imports without affecting the
|
||||
webpack build. This import looks like:
|
||||
|
||||
```
|
||||
<script>
|
||||
function browserImportFunction(moduleId) {
|
||||
return import(moduleId);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
and belongs in the root html file for your application.
|
||||
You then need to remove `dependencies` on the external import, and add a reference
|
||||
to the external import in your `pluginConfig.json` file.
|
||||
|
||||
### Example plugin config for `dicom-microscopy-viewer`
|
||||
The example below imports the `dicom-microscopy-viewer` for use as an external
|
||||
dependency. The example is part of the default `pluginConfig.json` file.
|
||||
|
||||
```
|
||||
"public": [
|
||||
{
|
||||
"directory": "./platform/public"
|
||||
},
|
||||
{
|
||||
"packageName": "dicom-microscopy-viewer",
|
||||
"importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js",
|
||||
"globalName": "dicomMicroscopyViewer",
|
||||
"directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
This defines two directory modules, whose contents are copied unchanged to the
|
||||
output build directory. It then defines the `dicom-microscopy-viewer` using
|
||||
the `packageName` element as being a module which is imported dynamically.
|
||||
Then, the import path passed into the browserImportFunction above is
|
||||
specified, and then how to access the import itself, via the `window.dicomMicroscopyViewer`
|
||||
global name reference.
|
||||
|
||||
### Referencing External Imports
|
||||
The appConfig either defines or has a default peerImport function which can be
|
||||
used to load references to the modules defined in the pluginConfig file. See
|
||||
the example in `init.tsx` for the cornerstone extension for how this is passed
|
||||
into CS3D for loading the whole slide imaging library.
|
||||
|
||||
### Usage of Dynamic Imports
|
||||
|
||||
|
||||
## BulkDataURI Configuration
|
||||
|
||||
We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one.
|
||||
|
||||
### What's Changing?
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before" label="Before 🕰️" default>
|
||||
|
||||
```javascript
|
||||
useBulkDataURI: false,
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After" label="After 🚀">
|
||||
|
||||
```javascript
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
// Additional configuration options
|
||||
},
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
Additional Notes:
|
||||
- The new configuration allows for more granular control over BulkDataURI behavior.
|
||||
- You can now add custom URL prefixing logic using the startsWith and prefixWith properties.
|
||||
- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems.
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
## Polyfill.io
|
||||
|
||||
We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-).
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
## Dynamic Modules
|
||||
|
||||
TBD
|
||||
|
||||
## Renaming some interfaces
|
||||
A few interfaces are being renamed to simple types to reflect the fact that
|
||||
they don't contain methods and are thus more properly simple types.
|
||||
|
||||
* IDisplaySet renamed to DisplaySet
|
||||
* Adding some field declarations to agree with actual usage
|
||||
|
||||
|
||||
## Use of ViewReference for navigation
|
||||
When navigating to measurements and storing/remembering navigation positions,
|
||||
the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable`
|
||||
used to check if a reference can be applied, and finally `viewport.setViewReference` to
|
||||
navigate to a view. Note that this changes the behaviour of navigation between
|
||||
MPR and Stack viewports, and also enables navigation of video and microscopy
|
||||
viewports in CS3D. This can cause some unexpected behaviour depending on how the
|
||||
frame of reference values are configured to allow for navigation.
|
||||
|
||||
The isReferenceViewable is used to determine when a view or measurement can be
|
||||
shown on a given view. For stack versus volume viewports, this can cause unexpected
|
||||
behaviour to be seen depending on how the view reference was fetched.
|
||||
|
||||
### `getViewReference` with `forFrameOfReference`
|
||||
When a view reference is fetched with the for frame of reference flag set to true,
|
||||
a reference will be returned which can be displayed on any viewport containing
|
||||
the same frame of reference and encompassing the given FOR and able to display the required
|
||||
orientation. Without this flag, a view reference is returned which will be
|
||||
displayed on a stack with the given image id, or a volume containing said image id
|
||||
or the specified volume.
|
||||
|
||||
### `isReferenceViewable` with navigation and/or orientation
|
||||
The is reference viewable will return false unless the given reference is directly
|
||||
viewable in the viewport as is. However, it can be passed various flags to determine
|
||||
whether the reference could be displayed if the viewport was modified in various ways,
|
||||
for example, by changing the position or orientation of the viewport. This allows
|
||||
checking for degrees of closeness so that the correct viewport can be chosen.
|
||||
|
||||
Note that this may result in displaying a measurement from one viewport on a completely
|
||||
different viewport, for example, showing a Probe tool from the stack viewport on
|
||||
an MPR view.
|
||||
@@ -142,7 +142,13 @@ this pattern, where multiple toolbar buttons are using the same evaluator but wi
|
||||
You can choose to set up multiple evaluators for a single button. This comes in handy when you need to assess the button according to various conditions. For example, we aim to prevent the Cine player from showing up on the 3D viewport, so we have:
|
||||
|
||||
```js
|
||||
evaluate: ['evaluate.cine', 'evaluate.not3D'],
|
||||
evaluate: [
|
||||
'evaluate.cine',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['volume3d'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
You can even come up with advanced evaluators such as:
|
||||
|
||||
@@ -20,11 +20,10 @@ There are seven events that get publish in `MeasurementService`:
|
||||
|
||||
| Event | Description |
|
||||
| --------------------- | ------------------------------------------------------ |
|
||||
| SEGMENTATION_UPDATED | Fires when a segmentation is updated e.g., segment added, removed etc.|
|
||||
| SEGMENTATION_MODIFIED | Fires when a segmentation is updated e.g., segment added, removed etc.|
|
||||
| SEGMENTATION_DATA_MODIFIED | Fires when the segmentation data changes |
|
||||
| SEGMENTATION_ADDED | Fires when a new segmentation is added to OHIF |
|
||||
| SEGMENTATION_REMOVED | Fires when a segmentation is removed from OHIF |
|
||||
| SEGMENTATION_CONFIGURATION_CHANGED | Fires when a segmentation configuration is changed |
|
||||
| SEGMENT_LOADING_COMPLETE | Fires when a segment group adds its pixel data to the volume |
|
||||
| SEGMENTATION_LOADING_COMPLETE | Fires when the full segmentation volume is filled with its segments |
|
||||
|
||||
@@ -33,7 +32,7 @@ There are seven events that get publish in `MeasurementService`:
|
||||
|
||||
### Segmentation Creation
|
||||
|
||||
- `createSegmentationForDisplaySet`: based on a reference displaySet, create a new segmentation. E.g., create a new segmentation based on a CT series
|
||||
- `createEmptyLabelmapForDisplaySetUID`: based on a reference displaySet, create a new segmentation. E.g., create a new segmentation based on a CT series
|
||||
- `createSegmentationForSEGDisplaySet`: given a segDisplaySet loaded by a sopClassHandler, create a new segmentation
|
||||
- `addSegmentationRepresentationToToolGroup`: given the toolGroupId, add the given segmentationId to the toolGroup.
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
sidebar_label: State Sync Service
|
||||
---
|
||||
|
||||
# State Sync Service
|
||||
|
||||
## Overview
|
||||
The state sync service is designed to allow short and long term memory of things such as
|
||||
annotations applied, last annotation state, hanging protocol viewport state,
|
||||
window level etc. This allows for better interaction with things like navigation
|
||||
between hanging protocols, ensuring that the previously displayed layouts
|
||||
can be redisplayed after returning to a given hanging protocol.
|
||||
|
||||
Currently, all the state sync service configurations have one of the following two
|
||||
lifetimes. See the mode description for general information on the mode lifetime.
|
||||
|
||||
* Application load - when the application is restarted, the state is lost
|
||||
* `clearOnModeExit` - which stores state until the mode onModeExit is called, and then throws away the remaining state. This is useful for mode specific information.
|
||||
|
||||
### TODO work - add more storage locations
|
||||
It is expected to add a few more storage locations, which will store to various
|
||||
locations on updates:
|
||||
|
||||
* User specific server store - to store things between application restarts at the user level
|
||||
* Browser state store - to store things in the browser local state, to recover after crashing.
|
||||
* Study specific server store - to store things relevant to a given study between application restarts, on the server.
|
||||
|
||||
## Events
|
||||
|
||||
Currently the service does not fire events.
|
||||
|
||||
## API
|
||||
|
||||
- `register`: to create a new named state storage
|
||||
- `reduce`: to apply a set of changes to several states at once
|
||||
- `getState`: to retrieve the current state
|
||||
- `onModeExit`: clears the states configured as clearOnModeExit states
|
||||
|
||||
### register
|
||||
The register call is typically added to an extension to create a new
|
||||
syncable state. A typical call is shown below, registering the viewport
|
||||
grid store state as a modal state.
|
||||
|
||||
```javascript
|
||||
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||
```
|
||||
|
||||
### getState
|
||||
The `getState` call returns an object containing all of the registered states,
|
||||
by id. The values can be read directly, but should not be modified.
|
||||
|
||||
### reduce
|
||||
The `reduce` call is used to apply a set of updates to various states. The
|
||||
updates are performed for every state as a simply "set" call.
|
||||
|
||||
### onModeExit
|
||||
When the Mode is exited, the onModeExit is called on the sync state, and this
|
||||
clears all states registered with `clearOnModeExit: true`.
|
||||
To avoid clearing the state, the mode definition should store any transient
|
||||
state in the mode onModeExit and recover it in the `mode.onModeEnter`.
|
||||
|
||||
## OHIF Registered State Sync Stores
|
||||
There are a number of defined stores here. It is recommended to update this
|
||||
list as state stores are added:
|
||||
|
||||
### Default Extension Stores
|
||||
|
||||
* `viewportGridStore` has viewport grid restore information for returning to an earlier grid layout.
|
||||
* `reuseIdMap` has a map of names to display sets for preserving user changes to hp display set selections.
|
||||
* `hanging` has a map of the hanging protocol stage information applied (HPInfo)
|
||||
|
||||
### Cornerstone Extension Stores
|
||||
|
||||
* `lutPresentationStore` has the cornerstone LUT (window level) presentation state information
|
||||
* `positionPresentationStore` has the cornerstone viewport position (camera, initial image) information
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
sidebar_label: Viewport Action Corners
|
||||
---
|
||||
|
||||
# Viewport Action Corners Service
|
||||
|
||||
The Viewport Action Corners Service is a powerful tool for managing interactive components in the corners of viewports within the OHIF viewer. This service allows developers to dynamically add, remove, and organize various UI elements such as menus, buttons, or custom components in specific locations around the viewport.
|
||||
|
||||
## Overview
|
||||
|
||||
The Viewport Action Corners Service extends the PubSubService and provides methods to:
|
||||
|
||||
- Add single or multiple components to viewport corners
|
||||
- Clear components from a specific viewport
|
||||
- Manage the state of viewport corner components
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Flexible Positioning**: Components can be placed in top-left, top-right, bottom-left, or bottom-right corners of the viewport.
|
||||
- **Priority Ordering**: Components can be assigned priority indices for ordering within a corner.
|
||||
- **Viewport-Specific**: Actions are associated with specific viewports, allowing for individualized control.
|
||||
- **Dynamic Updates**: Components can be added or removed at runtime, enabling context-sensitive UI elements.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the Viewport Action Corners Service, you typically interact with it through the `servicesManager`. Here's a basic example of how to add a component:
|
||||
|
||||
|
||||
Take a look at how we add window level menu to the top right corner of the viewport in the `OHIFCornerstoneViewport` component.
|
||||
@@ -11,10 +11,26 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const versions = fs.readFileSync('../../version.txt', 'utf8').split('\n');
|
||||
|
||||
const ArchivedVersionsDropdownItems = [
|
||||
{
|
||||
version: '2.0-deprecated',
|
||||
href: 'https://v2.docs.ohif.org',
|
||||
isExternal: true,
|
||||
},
|
||||
{
|
||||
version: '1.0-deprecated',
|
||||
href: 'https://v1.docs.ohif.org',
|
||||
isExternal: true,
|
||||
},
|
||||
];
|
||||
|
||||
const baseUrl = process.env.BASE_URL || '/';
|
||||
|
||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||
module.exports = {
|
||||
future: {
|
||||
experimental_faster: true,
|
||||
},
|
||||
title: 'OHIF',
|
||||
tagline: 'Open-source web-based medical imaging platform',
|
||||
organizationName: 'Open Health Imaging Foundation',
|
||||
@@ -47,22 +63,6 @@ module.exports = {
|
||||
// path.resolve(__dirname, './pluginOHIFWebpackConfig.js'),
|
||||
// /path.resolve(__dirname, './postcss.js'),
|
||||
'docusaurus-plugin-image-zoom', // 3rd party plugin for image click to pop
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
fromExtensions: ['html'],
|
||||
redirects: [
|
||||
{
|
||||
// we need this for https://cloud.google.com/healthcare/docs/how-tos/dicom-viewers
|
||||
to: '/2.0-deprecated/deployment/recipes/google-cloud-healthcare',
|
||||
from: [
|
||||
'/connecting-to-image-archives/google-cloud-healthcare',
|
||||
'/connecting-to-image-archives/google-cloud-healthcare.html',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'@docusaurus/plugin-ideal-image',
|
||||
{
|
||||
@@ -200,9 +200,16 @@ module.exports = {
|
||||
value: '<hr class="dropdown-separator">',
|
||||
},
|
||||
{
|
||||
to: '/versions',
|
||||
label: 'All versions',
|
||||
type: 'html',
|
||||
className: 'dropdown-archived-versions',
|
||||
value: '<b>Archived versions</b>',
|
||||
},
|
||||
...ArchivedVersionsDropdownItems.map(item => ({
|
||||
label: `${item.version} `,
|
||||
href: item.href,
|
||||
target: item.isExternal ? '_blank' : undefined,
|
||||
rel: item.isExternal ? 'noopener noreferrer' : undefined,
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -41,15 +41,16 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.5.2",
|
||||
"@docusaurus/plugin-client-redirects": "3.5.2",
|
||||
"@docusaurus/plugin-google-gtag": "3.5.2",
|
||||
"@docusaurus/plugin-ideal-image": "3.5.2",
|
||||
"@docusaurus/plugin-pwa": "3.5.2",
|
||||
"@docusaurus/preset-classic": "3.5.2",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "3.5.2",
|
||||
"@docusaurus/theme-classic": "3.5.2",
|
||||
"@docusaurus/theme-live-codeblock": "3.5.2",
|
||||
"@docusaurus/core": "3.6.0",
|
||||
"@docusaurus/plugin-client-redirects": "3.6.0",
|
||||
"@docusaurus/plugin-google-gtag": "3.6.0",
|
||||
"@docusaurus/plugin-ideal-image": "3.6.0",
|
||||
"@docusaurus/plugin-pwa": "3.6.0",
|
||||
"@docusaurus/preset-classic": "3.6.0",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "3.6.0",
|
||||
"@docusaurus/theme-classic": "3.6.0",
|
||||
"@docusaurus/theme-live-codeblock": "3.6.0",
|
||||
"@docusaurus/faster": "3.6.0",
|
||||
"@mdx-js/react": "3.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
|
||||
Loaded 100 of 803 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user