diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b896c54c..57876bea1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -180,10 +180,6 @@ jobs: - run: name: Authenticate with NPM registry command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc - - run: - name: Increase the event emitter limit - command: | - node ./increaseEventEmitterLimit.mjs - run: name: build half of the packages (to avoid out of memory in circleci) command: | @@ -230,10 +226,6 @@ jobs: - run: name: Authenticate with NPM registry command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc - - run: - name: Increase the event emitter limit - command: | - node ./increaseEventEmitterLimit.mjs - run: name: build half of the packages (to avoid out of memory in circleci) command: | diff --git a/.webpack/webpack.base.js b/.webpack/webpack.base.js index 10cca9597..b0ae242bd 100644 --- a/.webpack/webpack.base.js +++ b/.webpack/webpack.base.js @@ -144,11 +144,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => { }, ], }, - { - test: /\.js$/, - enforce: 'pre', - use: 'source-map-loader', - }, transpileJavaScriptRule(mode), loadWebWorkersRule, // loadShadersRule, @@ -188,8 +183,8 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => { '@hooks': path.resolve(__dirname, '../platform/app/src/hooks'), '@routes': path.resolve(__dirname, '../platform/app/src/routes'), '@state': path.resolve(__dirname, '../platform/app/src/state'), - '@cornerstonejs/dicom-image-loader': - '@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js', + 'dicom-microscopy-viewer': + 'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js', }, // Which directories to search when resolving modules modules: [ @@ -217,7 +212,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => { new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], }), - ...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin()]), + ...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]), // Uncomment to generate bundle analyzer // new BundleAnalyzerPlugin(), ], diff --git a/DATACITATION.md b/DATACITATION.md index 8c50b0dae..eff3cd8c3 100644 --- a/DATACITATION.md +++ b/DATACITATION.md @@ -288,3 +288,10 @@ Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Dat Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33 https://www.cancerimagingarchive.net/collection/ea1141/ + +### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21 + +From OFFIS DICOM-Team + +https://www.offis.de/ +OFFIS DICOM-Team diff --git a/addOns/externals/devDependencies/package.json b/addOns/externals/devDependencies/package.json index 2e1d41815..27c80a772 100644 --- a/addOns/externals/devDependencies/package.json +++ b/addOns/externals/devDependencies/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@kitware/vtk.js": "30.4.1", + "@kitware/vtk.js": "32.1.0", "clsx": "^2.1.1", "core-js": "^3.2.1", "moment": "^2.9.4" @@ -77,7 +77,7 @@ "stylus": "^0.59.0", "stylus-loader": "^7.1.3", "terser-webpack-plugin": "^5.1.4", - "typescript": "4.6.4", + "typescript": "5.5.4", "unused-webpack-plugin": "2.4.0", "webpack": "5.94.0", "webpack-bundle-analyzer": "^4.8.0", diff --git a/addOns/package.json b/addOns/package.json index d3d71f401..ad8818247 100644 --- a/addOns/package.json +++ b/addOns/package.json @@ -45,7 +45,7 @@ "glob-parent": "^6.0.2", "trim": "^1.0.0", "package-json": "^8.1.0", - "typescript": "4.6.4", + "typescript": "5.5.4", "sharp": "^0.32.6" } } diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index 5fd0cd70b..5647ad8ed 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.86.0", - "@cornerstonejs/core": "^1.86.0", - "@kitware/vtk.js": "30.4.1", + "@cornerstonejs/adapters": "^2.1.13", + "@cornerstonejs/core": "^2.1.13", + "@kitware/vtk.js": "32.1.0", "react-color": "^2.19.3" } } diff --git a/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts index 6f446b0d0..dc21ba3bf 100644 --- a/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts @@ -97,7 +97,7 @@ function _getDisplaySetsFromSeries( }; // Does not get the referenced volumeId during parametric displaySet creation because the - // referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`). + // referenced displaySet is still not available (getDisplaySetByUID returns `undefined`). displaySet.getReferencedVolumeId = () => { if (displaySet.referencedVolumeId) { return displaySet.referencedVolumeId; @@ -119,6 +119,22 @@ function _getDisplaySetsFromSeries( return [displaySet]; } +const getRangeFromPixelData = (pixelData: Float32Array) => { + let lowest = pixelData[0]; + let highest = pixelData[0]; + + for (let i = 1; i < pixelData.length; i++) { + if (pixelData[i] < lowest) { + lowest = pixelData[i]; + } + if (pixelData[i] > highest) { + highest = pixelData[i]; + } + } + + return [lowest, highest]; +}; + async function _load( displaySet, servicesManager: AppTypes.ServicesManager, @@ -132,11 +148,11 @@ async function _load( return volumeLoadObject.promise; } - displaySet.loadStatus.loading = true; + displaySet.loading = true; + displaySet.isLoaded = false; // We don't want to fire multiple loads, so we'll wait for the first to finish // and also return the same promise to any other callers. - // loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { const promise = _loadParametricMap({ extensionManager, displaySet, @@ -149,18 +165,26 @@ async function _load( promise .then(() => { - displaySet.loadStatus.loading = false; - displaySet.loadStatus.loaded = true; + displaySet.loading = false; + displaySet.isLoaded = true; + // Broadcast that loading is complete + servicesManager.services.segmentationService._broadcastEvent( + servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + { + pmapDisplaySet: displaySet, + } + ); }) .catch(err => { - displaySet.loadStatus.loading = false; + displaySet.loading = false; + displaySet.isLoaded = false; throw err; }); return promise; } -async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) { +async function _loadParametricMap({ displaySet, headers }: withAppTypes) { const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers); const referencedVolumeId = displaySet.getReferencedVolumeId(); const cachedReferencedVolume = cache.getVolume(referencedVolumeId); @@ -189,9 +213,12 @@ async function _loadParametricMap({ extensionManager, displaySet, headers }: wit }, }); - derivedVolume.getScalarData().set(pixelData); - - const range = derivedVolume.imageData.getPointData().getScalars().getRange(); + const newPixelData = new TypedArrayConstructor(pixelData.length); + for (let i = 0; i < pixelData.length; i++) { + newPixelData[i] = pixelData[i] * 100; + } + derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData); + const range = getRangeFromPixelData(newPixelData); const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]); derivedVolume.metadata.voiLut = [windowLevel]; diff --git a/extensions/cornerstone-dicom-pmap/src/utils/initPMAPToolGroup.ts b/extensions/cornerstone-dicom-pmap/src/utils/initPMAPToolGroup.ts deleted file mode 100644 index 6f0aa581e..000000000 --- a/extensions/cornerstone-dicom-pmap/src/utils/initPMAPToolGroup.ts +++ /dev/null @@ -1,6 +0,0 @@ -function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) { - const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {}; - return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools); -} - -export default createPMAPToolGroupAndAddTools; diff --git a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx index 9e0ee919f..2133e96dd 100644 --- a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx +++ b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx @@ -1,16 +1,19 @@ import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useRef } from 'react'; -import { useViewportGrid } from '@ohif/ui'; -import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup'; - -const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui'; function OHIFCornerstonePMAPViewport(props: withAppTypes) { - const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } = - props; + const { + displaySets, + children, + viewportOptions, + displaySetOptions, + servicesManager, + extensionManager, + } = props; const viewportId = viewportOptions.viewportId; - const { displaySetService, toolGroupService, customizationService } = servicesManager.services; - const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`; + const { displaySetService, segmentationService, uiNotificationService } = + servicesManager.services; // PMAP viewport will always have a single display set if (displaySets.length !== 1) { @@ -32,6 +35,24 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { metadata: referencedDisplaySetMetadata, }; + const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded); + + // Add effect to listen for loading complete + useEffect(() => { + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + evt => { + if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) { + setPmapIsLoading(false); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [pmapDisplaySet]); + const getCornerstoneViewport = useCallback(() => { const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current; const { component: Component } = extensionManager.getModuleEntry( @@ -46,12 +67,25 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { Object.assign(pmapDisplaySetOptions.options, { colormap: { - name: 'rainbow', + name: 'rainbow_2', opacity: [ - { value: 0, opacity: 0.5 }, - { value: 1, opacity: 1 }, + { value: 0, opacity: 0 }, + { value: 0.25, opacity: 0.25 }, + { value: 0.5, opacity: 0.5 }, + { value: 0.75, opacity: 0.75 }, + { value: 0.9, opacity: 0.99 }, ], }, + voi: { + windowCenter: 50, + windowWidth: 100, + }, + }); + + uiNotificationService.show({ + title: 'Parametric Map', + type: 'warning', + message: 'The values are multiplied by 100 in the viewport for better visibility', }); return ( @@ -61,7 +95,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { displaySets={[referencedDisplaySet, pmapDisplaySet]} viewportOptions={{ viewportType: 'volume', - toolGroupId: toolGroupId, orientation: viewportOptions.orientation, viewportId: viewportOptions.viewportId, }} @@ -73,7 +106,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { displaySetOptions, props, pmapDisplaySet, - toolGroupId, viewportOptions.orientation, viewportOptions.viewportId, ]); @@ -98,23 +130,33 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { }; }, [activeViewportId, displaySetService, viewportGridService, viewports]); - useEffect(() => { - let toolGroup = toolGroupService.getToolGroup(toolGroupId); + let childrenWithProps = null; - if (toolGroup) { - return; - } - - // This creates a custom tool group which has the lifetime of this view only - toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId); - - return () => toolGroupService.destroyToolGroup(toolGroupId); - }, [customizationService, toolGroupId, toolGroupService]); + if (children && children.length) { + childrenWithProps = children.map((child, index) => { + return ( + child && + React.cloneElement(child, { + viewportId, + key: index, + }) + ); + }); + } return ( <>
+ {pmapIsLoading && ( + + )} {getCornerstoneViewport()} + {childrenWithProps}
); diff --git a/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts b/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts new file mode 100644 index 000000000..d2737c555 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts @@ -0,0 +1,55 @@ +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; + +const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { + const services = servicesManager.services; + const { displaySetService, viewportGridService } = services; + + const actions = { + hydrateRTSDisplaySet: ({ displaySet, viewportId }) => { + if (displaySet.Modality !== 'RTSTRUCT') { + throw new Error('Display set is not an RTSTRUCT'); + } + + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + displaySet.referencedDisplaySetInstanceUID + ); + + // update the previously stored segmentationPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will have the correct segmentation representation hydrated + commandsManager.runCommand('updateStoredSegmentationPresentation', { + displaySet: displaySet, + type: SegmentationRepresentations.Contour, + }); + + // update the previously stored positionPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will be in the correct position zoom and pan + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId, + displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID, + }); + + viewportGridService.setDisplaySetsForViewport({ + viewportId, + displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], + }); + }, + }; + + const definitions = { + hydrateRTSDisplaySet: { + commandFn: actions.hydrateRTSDisplaySet, + storeContexts: [], + options: {}, + }, + }; + + return { + actions, + definitions, + defaultContext: 'cornerstone-dicom-rt', + }; +}; + +export default commandsModule; diff --git a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts index 6f65a1bf5..505819d43 100644 --- a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts @@ -72,22 +72,27 @@ function _getDisplaySetsFromSeries( displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; - displaySet.getReferenceDisplaySet = () => { - const { displaySetService } = servicesManager.services; - const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( - displaySet.referencedSeriesInstanceUID + const { displaySetService } = servicesManager.services; + const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( + displaySet.referencedSeriesInstanceUID + ); + + if (!referencedDisplaySets || referencedDisplaySets.length === 0) { + // Instead of throwing error, subscribe to display sets added + const { unsubscribe } = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + ({ displaySetsAdded }) => { + const addedDisplaySet = displaySetsAdded[0]; + if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { + displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; + unsubscribe(); + } + } ); - - if (!referencedDisplaySets || referencedDisplaySets.length === 0) { - throw new Error('Referenced DisplaySet is missing for the RT'); - } - + } else { const referencedDisplaySet = referencedDisplaySets[0]; - displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; - - return referencedDisplaySet; - }; + } displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers); @@ -111,19 +116,13 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio // and also return the same promise to any other callers. loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { if (!rtDisplaySet.structureSet) { - const structureSet = await loadRTStruct( - extensionManager, - rtDisplaySet, - rtDisplaySet.getReferenceDisplaySet(), - headers - ); + const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers); rtDisplaySet.structureSet = structureSet; } - const suppressEvents = true; segmentationService - .createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents) + .createSegmentationForRTDisplaySet(rtDisplaySet) .then(() => { rtDisplaySet.loading = false; resolve(); diff --git a/extensions/cornerstone-dicom-rt/src/index.tsx b/extensions/cornerstone-dicom-rt/src/index.tsx index 953c9a7c7..6d4e4db2e 100644 --- a/extensions/cornerstone-dicom-rt/src/index.tsx +++ b/extensions/cornerstone-dicom-rt/src/index.tsx @@ -2,6 +2,7 @@ import { id } from './id'; import React from 'react'; import { Types } from '@ohif/core'; import getSopClassHandlerModule from './getSopClassHandlerModule'; +import getCommandsModule from './getCommandsModule'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'); @@ -24,6 +25,7 @@ const extension: Types.Extensions.Extension = { * You ID can be anything you want, but it should be unique. */ id, + getCommandsModule, /** * PanelModule should provide a list of panels that will be available in OHIF diff --git a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js index cf58eb40a..10d9cc99f 100644 --- a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js +++ b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js @@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) { }); } -export default async function loadRTStruct( - extensionManager, - rtStructDisplaySet, - referencedDisplaySet, - headers -) { +export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) { const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.common' ); @@ -102,8 +97,6 @@ export default async function loadRTStruct( const { bulkDataURI } = dataSource.getConfig?.() || {}; const { dicomLoaderService } = utilityModule.exports; - const imageIdSopInstanceUidPairs = - _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet); // Set here is loading is asynchronous. // If this function throws its set back to false. @@ -149,8 +142,7 @@ export default async function loadRTStruct( const contourPoints = []; for (let c = 0; c < ContourSequenceArray.length; c++) { - const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } = - ContourSequenceArray[c]; + const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c]; let isSupported = false; @@ -194,24 +186,6 @@ export default async function loadRTStruct( return structureSet; } -const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => { - const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find( - imageIdSopInstanceUidPairsEntry => - imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID - ); - - return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null; -}; - -function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) { - return referencedDisplaySet.images.map(image => { - return { - imageId: image.imageId, - sopInstanceUID: image.SOPInstanceUID, - }; - }); -} - function _setROIContourMetadata( structureSet, StructureSetROISequence, diff --git a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts index 972f62f25..89abd66df 100644 --- a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts +++ b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts @@ -10,7 +10,6 @@ function promptHydrateRT({ servicesManager, rtDisplaySet, viewportId, - toolGroupId = 'default', preHydrateCallbacks, hydrateRTDisplaySet, }: withAppTypes) { @@ -27,16 +26,13 @@ function promptHydrateRT({ callback(); }); - window.setTimeout(async () => { - const isHydrated = await hydrateRTDisplaySet({ - rtDisplaySet, - viewportId, - toolGroupId, - servicesManager, - }); + const isHydrated = await hydrateRTDisplaySet({ + rtDisplaySet, + viewportId, + servicesManager, + }); - resolve(isHydrated); - }, 0); + resolve(isHydrated); } }); } diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index ab8ee32a5..1c4a9e0e9 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -4,7 +4,9 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f import promptHydrateRT from '../utils/promptHydrateRT'; import _getStatusComponent from './_getStatusComponent'; + import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; @@ -62,7 +64,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { const { viewports, activeViewportId } = viewportGrid; - const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet(); + const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet); referencedDisplaySetRef.current = { @@ -91,12 +96,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { }); }, [viewportGrid]); - const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => { - commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { - displaySets: [rtDisplaySet], - viewportId, - }); - }; + const hydrateRTDisplaySet = useCallback( + ({ rtDisplaySet, viewportId }) => { + commandsManager.runCommand('hydrateRTSDisplaySet', { + displaySet: rtDisplaySet, + viewportId, + }); + }, + [commandsManager] + ); const getCornerstoneViewport = useCallback(() => { const { component: Component } = extensionManager.getModuleEntry( @@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { newSelectedSegmentIndex = numberOfSegments - 1; } - segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId); + segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId); setSelectedSegment(newSelectedSegmentIndex); }, - [selectedSegment] + [selectedSegment, segmentationService] ); useEffect(() => { @@ -243,7 +251,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { return () => { // remove the segmentation representations if seg displayset changed - segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId); + segmentationService.removeSegmentationRepresentations(viewportId); toolGroupService.destroyToolGroup(toolGroupId); }; @@ -254,11 +262,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { return () => { // remove the segmentation representations if seg displayset changed - segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId); + segmentationService.removeSegmentationRepresentations(viewportId); referencedDisplaySetRef.current = null; }; }, [rtDisplaySet]); + const onStatusClick = useCallback(async () => { + // Before hydrating a RT and make it added to all viewports in the grid + // that share the same frameOfReferenceUID, we need to store the viewport grid + // presentation state, so that we can restore it after hydrating the RT. This is + // required if the user has changed the viewport (other viewport than RT viewport) + // presentation state (w/l and invert) and then opens the RT. If we don't store + // the presentation state, the viewport will be reset to the default presentation + storePresentationState(); + const isHydrated = await hydrateRTDisplaySet({ + rtDisplaySet, + viewportId, + }); + + setIsHydrated(isHydrated); + }, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ let childrenWithProps = null; @@ -282,24 +306,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { }); } - const onStatusClick = useCallback(async () => { - // Before hydrating a RT and make it added to all viewports in the grid - // that share the same frameOfReferenceUID, we need to store the viewport grid - // presentation state, so that we can restore it after hydrating the RT. This is - // required if the user has changed the viewport (other viewport than RT viewport) - // presentation state (w/l and invert) and then opens the RT. If we don't store - // the presentation state, the viewport will be reset to the default presentation - storePresentationState(); - const isHydrated = await hydrateRTDisplaySet({ - rtDisplaySet, - viewportId, - }); - - setIsHydrated(isHydrated); - }, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]); - useEffect(() => { - viewportActionCornersService.setComponents([ + viewportActionCornersService.addComponents([ { viewportId, id: 'viewportStatusComponent', @@ -318,7 +326,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { key="actionArrows" onArrowsClick={onSegmentChange} className={ - viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible' + viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible' } > ), diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index b367ddc82..2be604769 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.86.0", - "@cornerstonejs/core": "^1.86.0", - "@kitware/vtk.js": "30.4.1", + "@cornerstonejs/adapters": "^2.1.13", + "@cornerstonejs/core": "^2.1.13", + "@kitware/vtk.js": "32.1.0", "react-color": "^2.19.3" } } diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index e4c74ea07..b3c63ed13 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -14,18 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; -import { - updateViewportsForSegmentationRendering, - getUpdatedViewportsForSegmentation, - getTargetViewport, -} from './utils/hydrationUtils'; const { segmentation: segmentationUtils } = utilities; const { datasetToBlob } = dcmjs.data; +const getTargetViewport = ({ viewportId, viewportGridService }) => { + const { viewports, activeViewportId } = viewportGridService.getState(); + const targetViewportId = viewportId || activeViewportId; + + const viewport = viewports.get(targetViewportId); + + return viewport; +}; + const { Cornerstone3D: { - Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation }, + Segmentation: { generateSegmentation }, }, } = adaptersSEG; @@ -42,87 +46,14 @@ const commandsModule = ({ extensionManager, }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { const { - uiNotificationService, segmentationService, uiDialogService, displaySetService, viewportGridService, toolGroupService, - cornerstoneViewportService, - } = servicesManager.services; + } = servicesManager.services as AppTypes.Services; const actions = { - /** - * Retrieves a list of viewports that require updates in preparation for segmentation rendering. - * This function evaluates viewports based on their compatibility with the provided segmentation's - * frame of reference UID and appends them to the updated list if they should render the segmentation. - * - * @param {Object} params - Parameters for the function. - * @param params.viewportId - the ID of the viewport to be updated. - * @param params.servicesManager - The services manager - * @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance. - * - * @returns {Array} Returns an array of viewports that require updates for segmentation rendering. - */ - getUpdatedViewportsForSegmentation, - /** - * Creates an empty segmentation for a specified viewport. - * It first checks if the display set associated with the viewport is reconstructable. - * If not, it raises a notification error. Otherwise, it creates a new segmentation - * for the display set after handling the necessary steps for making the viewport - * a volume viewport first - * - * @param {Object} params - Parameters for the function. - * @param params.viewportId - the target viewport ID. - * - */ - createEmptySegmentationForViewport: async ({ viewportId }) => { - const viewport = getTargetViewport({ viewportId, viewportGridService }); - // Todo: add support for multiple display sets - const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; - - const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); - - if (!displaySet.isReconstructable) { - uiNotificationService.show({ - title: 'Segmentation', - message: 'Segmentation is not supported for non-reconstructible displaysets yet', - type: 'error', - }); - return; - } - - updateViewportsForSegmentationRendering({ - viewportId, - servicesManager, - displaySet, - loadFn: async () => { - const currentSegmentations = segmentationService.getSegmentations(); - const segmentationId = await segmentationService.createSegmentationForDisplaySet( - displaySetInstanceUID, - { label: `Segmentation ${currentSegmentations.length + 1}` } - ); - - const toolGroupId = viewport.viewportOptions.toolGroupId; - - await segmentationService.addSegmentationRepresentationToToolGroup( - toolGroupId, - segmentationId - ); - - // Add only one segment for now - segmentationService.addSegment(segmentationId, { - toolGroupId, - segmentIndex: 1, - properties: { - label: 'Segment 1', - }, - }); - - return segmentationId; - }, - }); - }, /** * Loads segmentations for a specified viewport. * The function prepares the viewport for rendering, then loads the segmentation details. @@ -134,106 +65,30 @@ const commandsModule = ({ * */ loadSegmentationsForViewport: async ({ segmentations, viewportId }) => { - updateViewportsForSegmentationRendering({ - viewportId, - servicesManager, - loadFn: async () => { - // Todo: handle adding more than one segmentation - const viewport = getTargetViewport({ viewportId, viewportGridService }); - const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; - - const segmentation = segmentations[0]; - const segmentationId = segmentation.id; - const label = segmentation.label; - const segments = segmentation.segments; - - delete segmentation.segments; - - await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, { - segmentationId, - label, - }); - - if (segmentation.scalarData) { - const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId); - labelmapVolume.scalarData.set(segmentation.scalarData); - } - - segmentationService.addOrUpdateSegmentation(segmentation); - - const toolGroupId = viewport.viewportOptions.toolGroupId; - await segmentationService.addSegmentationRepresentationToToolGroup( - toolGroupId, - segmentationId - ); - - segments.forEach(segment => { - if (segment === null) { - return; - } - segmentationService.addSegment(segmentationId, { - segmentIndex: segment.segmentIndex, - toolGroupId, - properties: { - color: segment.color, - label: segment.label, - opacity: segment.opacity, - isLocked: segment.isLocked, - visibility: segment.isVisible, - active: segmentation.activeSegmentIndex === segment.segmentIndex, - }, - }); - }); - - if (segmentation.centroidsIJK) { - segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK); - } - - return segmentationId; - }, - }); - }, - /** - * Loads segmentation display sets for a specified viewport. - * Depending on the modality of the display set (SEG or RTSTRUCT), - * it chooses the appropriate service function to create - * the segmentation for the display set. - * The function then prepares the viewport for rendering segmentation. - * - * @param {Object} params - Parameters for the function. - * @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded. - * @param params.displaySets - Array of display sets to be loaded for segmentation. - * - */ - loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => { // Todo: handle adding more than one segmentation - const displaySet = displaySets[0]; - const referencedDisplaySet = displaySetService.getDisplaySetByUID( - displaySet.referencedDisplaySetInstanceUID - ); - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - const initialSliceIndex = viewport.getSliceIndex(); + const viewport = getTargetViewport({ viewportId, viewportGridService }); + const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; - updateViewportsForSegmentationRendering({ - viewportId, - servicesManager, - displaySet, - loadFn: async () => { - const segDisplaySet = displaySet; - const suppressEvents = false; - const serviceFunction = - segDisplaySet.Modality === 'SEG' - ? 'createSegmentationForSEGDisplaySet' - : 'createSegmentationForRTDisplaySet'; + const segmentation = segmentations[0]; + const segmentationId = segmentation.segmentationId; + const label = segmentation.config.label; + const segments = segmentation.config.segments; - const boundFn = segmentationService[serviceFunction].bind(segmentationService); - const segmentationId = await boundFn(segDisplaySet, null, suppressEvents); - const segmentation = segmentationService.getSegmentation(segmentationId); - segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`; - return segmentationId; - }, - initialSliceIndex, + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + await segmentationService.createLabelmapForDisplaySet(displaySet, { + segmentationId, + segments, + label, }); + + segmentationService.addOrUpdateSegmentation(segmentation); + + await segmentationService.addSegmentationRepresentation(viewport.viewportId, { + segmentationId, + }); + + return segmentationId; }, /** * Generates a segmentation from a given segmentation ID. @@ -251,25 +106,61 @@ const commandsModule = ({ generateSegmentation: ({ segmentationId, options = {} }) => { const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); - const { referencedVolumeId } = segmentation.representationData.LABELMAP; + const { imageIds } = segmentation.representationData.Labelmap; - const segmentationVolume = cache.getVolume(segmentationId); - const referencedVolume = cache.getVolume(referencedVolumeId); - const referencedImages = referencedVolume.getCornerstoneImages(); + const segImages = imageIds.map(imageId => cache.getImage(imageId)); + const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId)); - const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); + const labelmaps2D = []; - // Generate fake metadata as an example - labelmapObj.metadata = []; + let z = 0; + + for (const segImage of segImages) { + const segmentsOnLabelmap = new Set(); + const pixelData = segImage.getPixelData(); + const { rows, columns } = segImage; + + // Use a single pass through the pixel data + for (let i = 0; i < pixelData.length; i++) { + const segment = pixelData[i]; + if (segment !== 0) { + segmentsOnLabelmap.add(segment); + } + } + + labelmaps2D[z++] = { + segmentsOnLabelmap: Array.from(segmentsOnLabelmap), + pixelData, + rows, + columns, + }; + } + + const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap); + + const labelmap3D = { + segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())), + metadata: [], + labelmaps2D, + }; const segmentationInOHIF = segmentationService.getSegmentation(segmentationId); - segmentationInOHIF.segments.forEach(segment => { + const representations = segmentationService.getRepresentationsForSegmentation(segmentationId); + + Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => { // segmentation service already has a color for each segment if (!segment) { return; } - const segmentIndex = segment.segmentIndex; - const { label, color } = segment; + + const { label } = segment; + + const firstRepresentation = representations[0]; + const color = segmentationService.getSegmentColor( + firstRepresentation.viewportId, + segmentationId, + segment.segmentIndex + ); const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( color.slice(0, 3).map(value => value / 255) @@ -292,12 +183,12 @@ const commandsModule = ({ CodeMeaning: 'Tissue', }, }; - labelmapObj.metadata[segmentIndex] = segmentMetadata; + labelmap3D.metadata[segmentIndex] = segmentMetadata; }); const generatedSegmentation = generateSegmentation( referencedImages, - labelmapObj, + labelmap3D, metaData, options ); @@ -445,18 +336,19 @@ const commandsModule = ({ }; const definitions = { - getUpdatedViewportsForSegmentation: { - commandFn: actions.getUpdatedViewportsForSegmentation, - }, + /** + * Obsolete? + */ loadSegmentationDisplaySetsForViewport: { commandFn: actions.loadSegmentationDisplaySetsForViewport, }, + /** + * Obsolete? + */ loadSegmentationsForViewport: { commandFn: actions.loadSegmentationsForViewport, }, - createEmptySegmentationForViewport: { - commandFn: actions.createEmptySegmentationForViewport, - }, + generateSegmentation: { commandFn: actions.generateSegmentation, }, diff --git a/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts b/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts index f61136245..700153f77 100644 --- a/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts @@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = { viewportType: 'stack', toolGroupId: 'default', allowUnmatchedView: true, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + // options: { + // matchingRules: ['sameFOR'], + // }, + }, + ], }, displaySets: [ { @@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = { }, viewports: [ { - viewportOptions: { allowUnmatchedView: true }, + viewportOptions: { + allowUnmatchedView: true, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + // options: { + // matchingRules: ['sameFOR'], + // }, + }, + ], + }, displaySets: [ { id: 'segDisplaySetId', diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts index 8aaf074e0..746c5409e 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts @@ -1,6 +1,6 @@ import { utils } from '@ohif/core'; -import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core'; -import { CONSTANTS } from '@cornerstonejs/tools'; +import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core'; +import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; import { SOPClassHandlerId } from './id'; @@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries( displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; + const { displaySetService } = servicesManager.services; + const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( + displaySet.referencedSeriesInstanceUID + ); - displaySet.getReferenceDisplaySet = () => { - const { displaySetService } = servicesManager.services; - const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( - displaySet.referencedSeriesInstanceUID + const referencedDisplaySet = referencedDisplaySets[0]; + + if (!referencedDisplaySet) { + // subscribe to display sets added which means at some point it will be available + const { unsubscribe } = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + ({ displaySetsAdded }) => { + // here we can also do a little bit of search, since sometimes DICOM SEG + // does not contain the referenced display set uid , and we can just + // see which of the display sets added is more similar and assign it + // to the referencedDisplaySet + const addedDisplaySet = displaySetsAdded[0]; + if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { + displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; + unsubscribe(); + } + } ); - - if (!referencedDisplaySets || referencedDisplaySets.length === 0) { - throw new Error('Referenced DisplaySet is missing for the SEG'); - } - - const referencedDisplaySet = referencedDisplaySets[0]; - + } else { displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; - - // Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc. - displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID; - const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`; - displaySet.referencedVolumeId = referencedVolumeId; - - return referencedDisplaySet; - }; + } displaySet.load = async ({ headers }) => await _load(displaySet, servicesManager, extensionManager, headers); @@ -111,7 +115,7 @@ function _load( if ( (segDisplaySet.loading || segDisplaySet.isLoaded) && loadPromises[SOPInstanceUID] && - _segmentationExists(segDisplaySet, segmentationService) + _segmentationExists(segDisplaySet) ) { return loadPromises[SOPInstanceUID]; } @@ -135,9 +139,8 @@ function _load( } } - const suppressEvents = true; segmentationService - .createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents) + .createSegmentationForSEGDisplaySet(segDisplaySet) .then(() => { segDisplaySet.loading = false; resolve(); @@ -166,20 +169,20 @@ async function _loadSegments({ const { dicomLoaderService } = utilityModule.exports; const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); - const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId); + const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID( + segDisplaySet.referencedDisplaySetInstanceUID + ); - if (!cachedReferencedVolume) { - throw new Error( - 'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet' - ); + if (!referencedDisplaySet) { + throw new Error('referencedDisplaySet is missing for SEG'); } - const { imageIds } = cachedReferencedVolume; + const { instances: images } = referencedDisplaySet; + const imageIds = images.map(({ imageId }) => imageId); // Todo: what should be defaults here const tolerance = 0.001; const skipOverlapping = true; - eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => { const { percentComplete } = evt.detail; segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, { @@ -231,9 +234,8 @@ async function _loadSegments({ Object.assign(segDisplaySet, results); } -function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) { - // This should be abstracted with the CornerstoneCacheService - return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID); +function _segmentationExists(segDisplaySet) { + return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID); } function getSopClassHandlerModule({ servicesManager, extensionManager }) { diff --git a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts index e991d778c..ab1dcd58b 100644 --- a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts @@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { // forcing the button to have black background since initially // it is designed for the toolbox not the toolbar on top // we should then branch the buttonSectionId to have different styles - const segmentations = segmentationService.getSegmentations(); + const segmentations = segmentationService.getSegmentationRepresentations(viewportId); if (!segmentations?.length) { return { disabled: true, diff --git a/extensions/cornerstone-dicom-seg/src/index.tsx b/extensions/cornerstone-dicom-seg/src/index.tsx index 02db57545..e1315b631 100644 --- a/extensions/cornerstone-dicom-seg/src/index.tsx +++ b/extensions/cornerstone-dicom-seg/src/index.tsx @@ -3,7 +3,6 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; import getHangingProtocolModule from './getHangingProtocolModule'; -import getPanelModule from './getPanelModule'; import getCommandsModule from './commandsModule'; import { getToolbarModule } from './getToolbarModule'; @@ -28,13 +27,6 @@ const extension = { * You ID can be anything you want, but it should be unique. */ id, - /** - * PanelModule should provide a list of panels that will be available in OHIF - * for Modes to consume and render. Each panel is defined by a {name, - * iconName, iconLabel, label, component} object. Example of a panel module - * is the StudyBrowserPanel that is provided by the default extension in OHIF. - */ - getPanelModule, getCommandsModule, getToolbarModule, getViewportModule({ servicesManager, extensionManager, commandsManager }) { diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx deleted file mode 100644 index 18300e870..000000000 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import { createReportAsync } from '@ohif/extension-default'; -import React, { useEffect, useState, useCallback } from 'react'; -import PropTypes from 'prop-types'; -import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui'; -import { SegmentationPanelMode } from '../types/segmentation'; -import callInputDialog from './callInputDialog'; -import callColorPickerDialog from './colorPickerDialog'; -import { useTranslation } from 'react-i18next'; -import { Separator } from '@ohif/ui-next'; - -const components = { - [SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded, - [SegmentationPanelMode.Dropdown]: SegmentationGroupTable, -}; - -export default function PanelSegmentation({ - servicesManager, - commandsManager, - extensionManager, - configuration, -}: withAppTypes) { - const { - segmentationService, - viewportGridService, - uiDialogService, - displaySetService, - cornerstoneViewportService, - } = servicesManager.services; - - const { t } = useTranslation('PanelSegmentation'); - - const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); - const [addSegmentationClassName, setAddSegmentationClassName] = useState(''); - const [segmentationConfiguration, setSegmentationConfiguration] = useState( - segmentationService.getConfiguration() - ); - - const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); - - useEffect(() => { - // ~~ Subscription - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; - const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED; - const subscriptions = []; - - [added, updated, removed].forEach(evt => { - const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); - setSegmentations(segmentations); - setSegmentationConfiguration(segmentationService.getConfiguration()); - }); - subscriptions.push(unsubscribe); - }); - - return () => { - subscriptions.forEach(unsub => { - unsub(); - }); - }; - }, []); - - // temporary measure to not allow add segmentation when the selected viewport - // is stack viewport - useEffect(() => { - const handleActiveViewportChange = viewportId => { - const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport( - viewportId || viewportGridService.getActiveViewportId() - ); - - if (!displaySetUIDs) { - return; - } - - const isReconstructable = - displaySetUIDs?.some(displaySetUID => { - const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); - return displaySet?.isReconstructable; - }) || false; - - if (isReconstructable) { - setAddSegmentationClassName(''); - } else { - setAddSegmentationClassName('ohif-disabled'); - } - }; - - // Handle initial state - handleActiveViewportChange(); - - const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED; - const ready = viewportGridService.EVENTS.VIEWPORTS_READY; - - const subsGrid = []; - [ready, changedGrid].forEach(evt => { - const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => { - handleActiveViewportChange(viewportId); - }); - - subsGrid.push(unsubscribe); - }); - - const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED; - - const subsData = []; - [changedData].forEach(evt => { - const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => { - handleActiveViewportChange(); - }); - - subsData.push(unsubscribe); - }); - - // Clean up - return () => { - subsGrid.forEach(unsub => unsub()); - subsData.forEach(unsub => unsub()); - }; - }, []); - - const getToolGroupIds = segmentationId => { - const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); - - return toolGroupIds; - }; - - const onSegmentationAdd = async () => { - commandsManager.runCommand('createEmptySegmentationForViewport', { - viewportId: viewportGridService.getActiveViewportId(), - }); - }; - - const onSegmentationClick = (segmentationId: string) => { - segmentationService.setActiveSegmentationForToolGroup(segmentationId); - }; - - const onSegmentationDelete = (segmentationId: string) => { - segmentationService.remove(segmentationId); - }; - - const onSegmentAdd = segmentationId => { - segmentationService.addSegment(segmentationId); - }; - - const onSegmentClick = (segmentationId, segmentIndex) => { - segmentationService.setActiveSegment(segmentationId, segmentIndex); - - const toolGroupIds = getToolGroupIds(segmentationId); - - toolGroupIds.forEach(toolGroupId => { - // const toolGroupId = - segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId); - segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId); - }); - }; - - const onSegmentEdit = (segmentationId, segmentIndex) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - - const segment = segmentation.segments[segmentIndex]; - const { label } = segment; - - callInputDialog(uiDialogService, label, (label, actionId) => { - if (label === '') { - return; - } - - segmentationService.setSegmentLabel(segmentationId, segmentIndex, label); - }); - }; - - const onSegmentationEdit = segmentationId => { - const segmentation = segmentationService.getSegmentation(segmentationId); - const { label } = segmentation; - - callInputDialog(uiDialogService, label, (label, actionId) => { - if (label === '') { - return; - } - - segmentationService.addOrUpdateSegmentation( - { - id: segmentationId, - label, - }, - false, // suppress event - true // notYetUpdatedAtSource - ); - }); - }; - - const onSegmentColorClick = (segmentationId, segmentIndex) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - - const segment = segmentation.segments[segmentIndex]; - const { color, opacity } = segment; - - const rgbaColor = { - r: color[0], - g: color[1], - b: color[2], - a: opacity / 255.0, - }; - - callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { - if (actionId === 'cancel') { - return; - } - - segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [ - newRgbaColor.r, - newRgbaColor.g, - newRgbaColor.b, - newRgbaColor.a * 255.0, - ]); - }); - }; - - const onSegmentDelete = (segmentationId, segmentIndex) => { - segmentationService.removeSegment(segmentationId, segmentIndex); - }; - - // segment hide - const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - const segmentInfo = segmentation.segments[segmentIndex]; - const isVisible = !segmentInfo.isVisible; - const toolGroupIds = getToolGroupIds(segmentationId); - - // Todo: right now we apply the visibility to all tool groups - toolGroupIds.forEach(toolGroupId => { - segmentationService.setSegmentVisibility( - segmentationId, - segmentIndex, - isVisible, - toolGroupId - ); - }); - }; - - const onToggleSegmentLock = (segmentationId, segmentIndex) => { - segmentationService.toggleSegmentLocked(segmentationId, segmentIndex); - }; - - const onToggleSegmentationVisibility = segmentationId => { - segmentationService.toggleSegmentationVisibility(segmentationId); - const segmentation = segmentationService.getSegmentation(segmentationId); - const isVisible = segmentation.isVisible; - const segments = segmentation.segments; - - const toolGroupIds = getToolGroupIds(segmentationId); - - toolGroupIds.forEach(toolGroupId => { - segments.forEach((segment, segmentIndex) => { - segmentationService.setSegmentVisibility( - segmentationId, - segmentIndex, - isVisible, - toolGroupId - ); - }); - }); - }; - - const _setSegmentationConfiguration = useCallback( - (segmentationId, key, value) => { - segmentationService.setConfiguration({ - segmentationId, - [key]: value, - }); - }, - [segmentationService] - ); - - const onSegmentationDownload = segmentationId => { - commandsManager.runCommand('downloadSegmentation', { - segmentationId, - }); - }; - - const storeSegmentation = async segmentationId => { - const datasources = extensionManager.getActiveDataSource(); - - const displaySetInstanceUIDs = await createReportAsync({ - servicesManager, - getReport: () => - commandsManager.runCommand('storeSegmentation', { - segmentationId, - dataSource: datasources[0], - }), - reportType: 'Segmentation', - }); - - // Show the exported report in the active viewport as read only (similar to SR) - if (displaySetInstanceUIDs) { - // clear the segmentation that we exported, similar to the storeMeasurement - // where we remove the measurements and prompt again the user if they would like - // to re-read the measurements in a SR read only viewport - segmentationService.remove(segmentationId); - - viewportGridService.setDisplaySetsForViewport({ - viewportId: viewportGridService.getActiveViewportId(), - displaySetInstanceUIDs, - }); - } - }; - - const onSegmentationDownloadRTSS = segmentationId => { - commandsManager.runCommand('downloadRTSS', { - segmentationId, - }); - }; - - const SegmentationGroupTableComponent = - components[configuration?.segmentationPanelMode] || SegmentationGroupTable; - const allowAddSegment = configuration?.addSegment; - const onSegmentationAddWrapper = - configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function' - ? configuration?.onSegmentationAdd - : onSegmentationAdd; - - return ( - <> - - _setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value) - } - setOutlineOpacityActive={value => - _setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value) - } - setRenderFill={value => - _setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value) - } - setRenderInactiveSegmentations={value => - _setSegmentationConfiguration( - selectedSegmentationId, - 'renderInactiveSegmentations', - value - ) - } - setOutlineWidthActive={value => - _setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value) - } - setFillAlpha={value => - _setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value) - } - setFillAlphaInactive={value => - _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) - } - /> - - ); -} - -PanelSegmentation.propTypes = { - commandsManager: PropTypes.shape({ - runCommand: PropTypes.func.isRequired, - }), - servicesManager: PropTypes.shape({ - services: PropTypes.shape({ - segmentationService: PropTypes.shape({ - getSegmentation: PropTypes.func.isRequired, - getSegmentations: PropTypes.func.isRequired, - toggleSegmentationVisibility: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - EVENTS: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, -}; diff --git a/extensions/cornerstone-dicom-seg/src/panels/callInputDialog.tsx b/extensions/cornerstone-dicom-seg/src/panels/callInputDialog.tsx deleted file mode 100644 index 6de2470d8..000000000 --- a/extensions/cornerstone-dicom-seg/src/panels/callInputDialog.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { Input, Dialog, ButtonEnums } from '@ohif/ui'; - -function callInputDialog(uiDialogService, label, callback) { - const dialogId = 'enter-segment-label'; - - const onSubmitHandler = ({ action, value }) => { - switch (action.id) { - case 'save': - callback(value.label, action.id); - break; - case 'cancel': - callback('', action.id); - break; - } - uiDialogService.dismiss({ id: dialogId }); - }; - - if (uiDialogService) { - uiDialogService.create({ - id: dialogId, - centralize: true, - isDraggable: false, - showOverlay: true, - content: Dialog, - contentProps: { - title: 'Segment', - value: { label }, - noCloseButton: true, - onClose: () => uiDialogService.dismiss({ id: dialogId }), - actions: [ - { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, - { id: 'save', text: 'Confirm', type: ButtonEnums.type.primary }, - ], - onSubmit: onSubmitHandler, - body: ({ value, setValue }) => { - return ( - { - event.persist(); - setValue(value => ({ ...value, label: event.target.value })); - }} - onKeyPress={event => { - if (event.key === 'Enter') { - onSubmitHandler({ value, action: { id: 'save' } }); - } - }} - /> - ); - }, - }, - }); - } -} - -export default callInputDialog; diff --git a/extensions/cornerstone-dicom-seg/src/panels/segmentationConfigReducer.tsx b/extensions/cornerstone-dicom-seg/src/panels/segmentationConfigReducer.tsx deleted file mode 100644 index 02857e86b..000000000 --- a/extensions/cornerstone-dicom-seg/src/panels/segmentationConfigReducer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Todo: use defaults in cs3d -const initialState = { - renderOutline: true, - renderFill: true, - outlineOpacity: 0.9, - outlineWidth: 3, - fillOpacity: 0.9, - fillOpacityInactive: 0.8, - renderInactiveSegmentations: true, -}; - -const reducer = (state, action) => { - switch (action.type) { - case 'RENDER_OUTLINE': - return { ...state, renderOutline: action.payload.value }; - case 'RENDER_FILL': - return { ...state, renderFill: action.payload.value }; - case 'SET_OUTLINE_OPACITY': - return { ...state, outlineOpacity: action.payload.value }; - case 'SET_OUTLINE_WIDTH': - return { ...state, outlineWidth: action.payload.value }; - case 'SET_FILL_OPACITY': - return { ...state, fillOpacity: action.payload.value }; - case 'SET_FILL_OPACITY_INACTIVE': - return { ...state, fillOpacityInactive: action.payload.value }; - case 'RENDER_INACTIVE_SEGMENTATIONS': - return { ...state, renderInactiveSegmentations: action.payload.value }; - default: - return state; - } -}; - -export { initialState, reducer }; diff --git a/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts b/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts deleted file mode 100644 index 6dd33c1b3..000000000 --- a/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Enums, cache } from '@cornerstonejs/core'; - -/** - * Updates the viewports in preparation for rendering segmentations. - * Evaluates each viewport to determine which need modifications, - * then for those viewports, changes them to a volume type and ensures - * they are ready for segmentation rendering. - * - * @param {Object} params - Parameters for the function. - * @param params.viewportId - ID of the viewport to be updated. - * @param params.loadFn - Function to load the segmentation data. - * @param params.servicesManager - The services manager. - * @param params.displaySet - the display set. - * @param params.initialSliceIndex - The initial slice index. - * - * @returns Returns true upon successful update of viewports for segmentation rendering. - */ -async function updateViewportsForSegmentationRendering({ - viewportId, - loadFn, - servicesManager, - displaySet, - initialSliceIndex = null, -}: { - viewportId: string; - loadFn: () => Promise; - servicesManager: AppTypes.ServicesManager; - displaySet?: any; - initialSliceIndex?: number; -}) { - const { cornerstoneViewportService, segmentationService, viewportGridService } = - servicesManager.services; - - const viewport = getTargetViewport({ viewportId, viewportGridService }); - const targetViewportId = viewport.viewportOptions.viewportId; - - const referencedDisplaySetInstanceUID = - displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0]; - - const updatedViewports = getUpdatedViewportsForSegmentation({ - viewportId, - servicesManager, - displaySet, - }); - - // create Segmentation callback which needs to be waited until - // the volume is created (if coming from stack) - const createSegmentationForVolume = async () => { - const segmentationId = await loadFn(); - segmentationService.hydrateSegmentation(segmentationId); - }; - - // the reference volume that is used to draw the segmentation. so check if the - // volume exists in the cache (the target Viewport is already a volume viewport) - const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId => - volumeId.includes(referencedDisplaySetInstanceUID) - ); - - updatedViewports.forEach(async viewport => { - viewport.viewportOptions = { - ...viewport.viewportOptions, - viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume', - needsRerendering: true, - }; - const viewportId = viewport.viewportId; - - // maintain the prehydration slice on the target viewport only - if (viewportId === targetViewportId) { - viewport.viewportOptions.initialImageOptions = { - index: initialSliceIndex, - useOnce: true, - }; - } - - const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - const prevCamera = csViewport.getCamera(); - - // only run the createSegmentationForVolume for the targetViewportId - // since the rest will get handled by cornerstoneViewportService - if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) { - await createSegmentationForVolume(); - return; - } - - const createNewSegmentationWhenVolumeMounts = async evt => { - const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac => - ac.uid.includes(referencedDisplaySetInstanceUID) - ); - - // Note: make sure to re-grab the viewport since it might have changed - // during the time it took for the volume to be mounted, for instance - // the stack viewport has been changed to a volume viewport - const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - volumeViewport.setCamera(prevCamera); - - volumeViewport.element.removeEventListener( - Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - createNewSegmentationWhenVolumeMounts - ); - - if (!isTheActiveViewportVolumeMounted) { - // it means it is one of those other updated viewports so just update the camera - return; - } - - if (viewportId === targetViewportId) { - await createSegmentationForVolume(); - } - }; - - csViewport.element.addEventListener( - Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - createNewSegmentationWhenVolumeMounts - ); - }); - - // Set the displaySets for the viewports that require to be updated - viewportGridService.setDisplaySetsForViewports(updatedViewports); - - return true; -} - -const getTargetViewport = ({ viewportId, viewportGridService }) => { - const { viewports, activeViewportId } = viewportGridService.getState(); - const targetViewportId = viewportId || activeViewportId; - - const viewport = viewports.get(targetViewportId); - - return viewport; -}; - -/** - * Retrieves a list of viewports that require updates in preparation for segmentation rendering. - * This function evaluates viewports based on their compatibility with the provided segmentation's - * frame of reference UID and appends them to the updated list if they should render the segmentation. - * - * @param {Object} params - Parameters for the function. - * @param params.viewportId - the ID of the viewport to be updated. - * @param params.servicesManager - The services manager - * @param params.displaySet - the display set. - * - * @returns {Array} Returns an array of viewports that require updates for segmentation rendering. - */ -function getUpdatedViewportsForSegmentation({ - viewportId, - servicesManager, - displaySet, -}: withAppTypes) { - const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } = - servicesManager.services; - - const { viewports, isHangingProtocolLayout } = viewportGridService.getState(); - - const viewport = getTargetViewport({ viewportId, viewportGridService }); - const targetViewportId = viewport.viewportOptions.viewportId; - - const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs; - - const referenceDisplaySetInstanceUID = - displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0]; - - const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID); - const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID; - - const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( - targetViewportId, - referenceDisplaySetInstanceUID, - isHangingProtocolLayout - ); - - viewports.forEach((viewport, viewportId) => { - if ( - targetViewportId === viewportId || - updatedViewports.find(v => v.viewportId === viewportId) - ) { - return; - } - - const shouldDisplaySeg = segmentationService.shouldRenderSegmentation( - viewport.displaySetInstanceUIDs, - segmentationFrameOfReferenceUID - ); - - if (shouldDisplaySeg) { - updatedViewports.push({ - viewportId, - displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, - viewportOptions: { - viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume', - needsRerendering: true, - }, - }); - } - }); - - return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d'); -} - -export { - updateViewportsForSegmentationRendering, - getUpdatedViewportsForSegmentation, - getTargetViewport, -}; diff --git a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts index d29a066b7..9d3a8c8b6 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts @@ -11,7 +11,7 @@ function promptHydrateSEG({ segDisplaySet, viewportId, preHydrateCallbacks, - hydrateSEGDisplaySet, + hydrateCallback, }: withAppTypes) { const { uiViewportDialogService } = servicesManager.services; const extensionManager = servicesManager._extensionManager; @@ -28,7 +28,7 @@ function promptHydrateSEG({ }); window.setTimeout(async () => { - const isHydrated = await hydrateSEGDisplaySet({ + const isHydrated = await hydrateCallback({ segDisplaySet, viewportId, }); diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index c78017edc..bec52996b 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -1,10 +1,11 @@ -import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import promptHydrateSEG from '../utils/promptHydrateSEG'; import _getStatusComponent from './_getStatusComponent'; +import { useSegmentationPresentationStore } from '@ohif/extension-cornerstone'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup'; @@ -37,7 +38,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { } const segDisplaySet = displaySets[0]; - const [viewportGrid, viewportGridService] = useViewportGrid(); // States @@ -61,7 +61,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { const { viewports, activeViewportId } = viewportGrid; - const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet(); + const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata( referencedDisplaySet, segDisplaySet @@ -98,18 +102,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { '@ohif/extension-cornerstone.viewportModule.cornerstone' ); - const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current; - // Todo: jump to the center of the first segment return ( { props.onElementEnabled?.(evt); @@ -139,12 +142,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { newSelectedSegmentIndex = numberOfSegments - 1; } - segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId); + segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId); setSelectedSegment(newSelectedSegmentIndex); }, [selectedSegment] ); + const hydrateSEG = useCallback(() => { + // update the previously stored segmentationPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will have the correct segmentation representation hydrated + commandsManager.runCommand('updateStoredSegmentationPresentation', { + displaySet: segDisplaySet, + type: SegmentationRepresentations.Labelmap, + }); + + // update the previously stored positionPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will be in the correct position zoom and pan + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId, + displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID, + }); + + viewportGridService.setDisplaySetsForViewport({ + viewportId, + displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], + }); + }, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]); + useEffect(() => { if (segIsLoading) { return; @@ -155,15 +181,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { viewportId, segDisplaySet, preHydrateCallbacks: [storePresentationState], - hydrateSEGDisplaySet, + hydrateCallback: hydrateSEG, }).then(isHydrated => { if (isHydrated) { setIsHydrated(true); } }); - }, [servicesManager, viewportId, segDisplaySet, segIsLoading]); + }, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]); useEffect(() => { + // on new seg display set, remove all segmentations from all viewports + segmentationService.clearSegmentationRepresentations(viewportId); + const { unsubscribe } = segmentationService.subscribe( segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, evt => { @@ -222,6 +251,9 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { if (toolGroup) { return; } + // always start fresh for this viewport since it is special type of viewport + // that should only show one segmentation at a time. + segmentationService.clearSegmentationRepresentations(viewportId); // This creates a custom tool group which has the lifetime of this view // only, and does NOT interfere with currently displayed segmentations. @@ -229,33 +261,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { return () => { // remove the segmentation representations if seg displayset changed - segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId); + // e.g., another seg displayset is dragged into the viewport + segmentationService.clearSegmentationRepresentations(viewportId); // Only destroy the viewport specific implementation toolGroupService.destroyToolGroup(toolGroupId); }; }, []); - useEffect(() => { - setIsHydrated(segDisplaySet.isHydrated); - - return () => { - // remove the segmentation representations if seg displayset changed - segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId); - referencedDisplaySetRef.current = null; - }; - }, [segDisplaySet]); - - const hydrateSEGDisplaySet = useCallback( - ({ segDisplaySet, viewportId }) => { - commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { - displaySets: [segDisplaySet], - viewportId, - }); - }, - [commandsManager] - ); - const onStatusClick = useCallback(async () => { // Before hydrating a SEG and make it added to all viewports in the grid // that share the same frameOfReferenceUID, we need to store the viewport grid @@ -264,16 +277,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { // presentation state (w/l and invert) and then opens the SEG. If we don't store // the presentation state, the viewport will be reset to the default presentation storePresentationState(); - const isHydrated = await hydrateSEGDisplaySet({ - segDisplaySet, - viewportId, - }); - - setIsHydrated(isHydrated); - }, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]); + hydrateSEG(); + }, [storePresentationState, hydrateSEG]); useEffect(() => { - viewportActionCornersService.setComponents([ + viewportActionCornersService.addComponents([ { viewportId, id: 'viewportStatusComponent', @@ -292,7 +300,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { key="actionArrows" onArrowsClick={onSegmentChange} className={ - viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible' + viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible' } > ), @@ -332,18 +340,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { }); } - const { - PatientID, - PatientName, - PatientSex, - PatientAge, - SliceThickness, - ManufacturerModelName, - StudyDate, - SeriesDescription, - SpacingBetweenSlices, - } = referencedDisplaySetRef.current.metadata; - return ( <>
@@ -362,13 +358,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { ); } -OHIFCornerstoneSEGViewport.propTypes = { - displaySets: PropTypes.arrayOf(PropTypes.object), - viewportId: PropTypes.string.isRequired, - dataSource: PropTypes.object, - children: PropTypes.node, -}; - function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) { const { SharedFunctionalGroupsSequence } = segDisplaySet.instance; diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 2b07fb95b..41aa71bb9 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.86.0", - "@cornerstonejs/core": "^1.86.0", - "@cornerstonejs/tools": "^1.86.0", + "@cornerstonejs/adapters": "^2.1.13", + "@cornerstonejs/core": "^2.1.13", + "@cornerstonejs/tools": "^2.1.13", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx index 8e39e9fc0..7c63f346c 100644 --- a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx @@ -15,8 +15,15 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking' const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup'; function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { - const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } = - props; + const { + commandsManager, + children, + dataSource, + displaySets, + viewportOptions, + servicesManager, + extensionManager, + } = props; const [appConfig] = useAppConfig(); @@ -63,6 +70,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { trackedMeasurements = tracked?.[0]; sendTrackedMeasurementsEvent = tracked?.[1]; } + if (!sendTrackedMeasurementsEvent) { // if no panels from measurement-tracking extension is used, this code will run trackedMeasurements = null; @@ -72,6 +80,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { { servicesManager, extensionManager, appConfig }, displaySetInstanceUID ); + const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]); if (displaySets.length) { viewportGridService.setDisplaySetsForViewports([ @@ -84,6 +93,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { }; } + /** + * Todo: what is this, not sure what it does regarding the react aspect, + * it is updating a local variable? which is not state. + */ + const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0); /** * Store the tracking identifiers per viewport in order to be able to * show the SR measurements on the referenced image on the correct viewport, @@ -297,17 +311,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { updateSR(); }, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]); - /** - * Todo: what is this, not sure what it does regarding the react aspect, - * it is updating a local variable? which is not state. - */ - const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0); useEffect(() => { setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0); }, [trackedMeasurements]); useEffect(() => { - viewportActionCornersService.setComponents([ + viewportActionCornersService.addComponents([ { viewportId, id: 'viewportStatusComponent', diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 2e74751dc..c2e4ff315 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -12,7 +12,7 @@ import { utilities as csToolsUtils, } from '@cornerstonejs/tools'; import { Types, MeasurementService } from '@ohif/core'; -import { StackViewport } from '@cornerstonejs/core'; +import { StackViewport, utilities as csUtils } from '@cornerstonejs/core'; import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import SCOORD3DPointTool from './tools/SCOORD3DPointTool'; @@ -95,7 +95,7 @@ export default function init({ const imageIndex = (currentViewport as StackViewport) .getImageIds() .indexOf(referencedImageId); - csToolsUtils.jumpToSlice(currentViewport.element, { imageIndex }); + csUtils.jumpToSlice(currentViewport.element, { imageIndex }); } catch (error) { console.warn('Unable to jump to image based on measurement coordinate', error); } diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts index a8660d316..bc44814f7 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts @@ -45,11 +45,19 @@ export default function hydrateStructuredReport( displaySetInstanceUID ) { const annotationManager = CsAnnotation.state.getAnnotationManager(); - const disableEditing = appConfig?.disableEditing; const dataSource = extensionManager.getActiveDataSource()[0]; const { measurementService, displaySetService, customizationService } = servicesManager.services; const codingValues = customizationService.getCustomization('codingValues', {}); + + const { disableEditing } = customizationService.getCustomization( + 'PanelMeasurement.disableEditing', + { + id: 'default.disableEditing', + disableEditing: false, + } + ); + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); // TODO -> We should define a strict versioning somewhere. diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index da1e90570..1fc4a54b3 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -42,9 +42,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^1.86.0", - "@cornerstonejs/streaming-image-volume-loader": "^1.82.4", - "@cornerstonejs/tools": "^1.86.0", + "@cornerstonejs/core": "^2.1.13", + "@cornerstonejs/tools": "^2.1.13", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts b/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts index 1f013d986..78df25a66 100644 --- a/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts +++ b/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts @@ -110,7 +110,12 @@ function _getTimePointsData(volume) { return { timePoints, timePointsUnit }; } -function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) { +function _getSegmentationData( + segmentation, + volumesTimePointsCache, + { servicesManager }: { servicesManager: AppTypes.ServicesManager } +) { + const { displaySetService, segmentationService, viewportGridService } = servicesManager.services; const displaySets = displaySetService.getActiveDisplaySets(); const dynamic4DDisplaySet = displaySets.find(displaySet => { @@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe referencedDynamicVolume.imageIds[0] ); + const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId); + const maskVolumeId = segmentationVolume?.volumeId; + const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, { - maskVolumeId: segmentation.id, + maskVolumeId, }) as number[][]; const pixelCount = timeData.length; @@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe return []; } - // since we only use one segmentation representation per segmentationId - // it is fine to pick the first one - const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations( - segmentation.id - ); - - const segmentationRepresentationUID = - segmentationRepresentations[0].segmentationRepresentationUID; - - const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID( - segmentationRepresentationUID - ); - // Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex - const color = csTools.segmentation.config.color.getColorForSegmentIndex( - toolGroupId, - segmentationRepresentationUID, - 1 // segmentIndex - ); + // const color = csTools.segmentation.config.color.getSegmentIndexColor( + // segmentationRepresentationUID, + // 1 // segmentIndex + // ); + const viewportId = viewportGridService.getActiveViewportId(); + const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1); - const hexColor = cs.utilities.color.rgbToHex(...color); + const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]); let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume); if (!timePointsData) { @@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe }; } -function _getInstanceFromSegmentations(segmentations, displaySetService) { +function _getInstanceFromSegmentations(segmentations, { servicesManager }) { if (!segmentations.length) { return; } const volumesTimePointsCache = new WeakMap(); const segmentationsData = segmentations.map(segmentation => - _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) + _getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager }) ); const { date: seriesDate, time: seriesTime } = _getDateTimeStr(); @@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) { } function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void { - const { segmentationService, displaySetService } = servicesManager.services; + debugger; + const { segmentationService } = servicesManager.services; const segmentations = segmentationService.getSegmentations(); const { seriesMetadata, instance } = - _getInstanceFromSegmentations(segmentations, displaySetService) ?? {}; + _getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {}; if (seriesMetadata && instance) { // An event is triggered after adding the instance and the displaySet is created diff --git a/extensions/cornerstone-dynamic-volume/src/commandsModule.ts b/extensions/cornerstone-dynamic-volume/src/commandsModule.ts index 7828dd3f6..34422bebb 100644 --- a/extensions/cornerstone-dynamic-volume/src/commandsModule.ts +++ b/extensions/cornerstone-dynamic-volume/src/commandsModule.ts @@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { const stdDeviation = Math.sqrt(variance); return stdDeviation; } - // Iterate through each segmentation to get the timeData and ijkCoords - segmentations.forEach((segmentation, segmentationIndex) => { + segmentations.forEach(segmentation => { + const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId); const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, { - maskVolumeId: segmentation.id, + maskVolumeId: volume.volumeId, }) as number[][]; if (summaryStats) { @@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) { // for each voxel in the ROI, get the value at the current time point const voxelValues = []; + let sum = 0; + let minValue = Infinity; + let maxValue = -Infinity; + let minIndex = 0; + let maxIndex = 0; + + // Single pass through the data to collect all needed values for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) { - voxelValues.push(timeData[voxelIndex][timeIndex]); + const value = timeData[voxelIndex][timeIndex]; + voxelValues.push(value); + sum += value; + + if (value < minValue) { + minValue = value; + minIndex = voxelIndex; + } + if (value > maxValue) { + maxValue = value; + maxIndex = voxelIndex; + } } - mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels); - const minimum = Math.min(...voxelValues); - min.push(minimum); - minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]); - const maximum = Math.max(...voxelValues); - max.push(maximum); - maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]); + mean.push(sum / numVoxels); + min.push(minValue); + minIJK.push(ijkCoords[minIndex]); + max.push(maxValue); + maxIJK.push(ijkCoords[maxIndex]); std.push(calculateStandardDeviation(voxelValues)); } @@ -340,30 +356,19 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { throw new Error('No reference display set found based on the dynamic data'); } - const segmentationId = await segmentationService.createSegmentationForDisplaySet( - referenceDisplaySet.displaySetInstanceUID, - { label } + const displaySet = displaySetService.getDisplaySetByUID( + referenceDisplaySet.displaySetInstanceUID ); - // Add Segmentation to all toolGroupIds in the viewer - const toolGroupIds = Array.from( - viewports.values(), - viewport => viewport.viewportOptions.toolGroupId - ); + const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, { + label, + }); - const representationType = LABELMAP; + const firstViewport = viewports.values().next().value; - for (const toolGroupId of toolGroupIds) { - const hydrateSegmentation = true; - await segmentationService.addSegmentationRepresentationToToolGroup( - toolGroupId, - segmentationId, - hydrateSegmentation, - representationType - ); - - segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId); - } + await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, { + segmentationId, + }); return segmentationId; }, diff --git a/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts b/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts index 0a8b6d520..4ee5ed04b 100644 --- a/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts +++ b/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts @@ -147,8 +147,8 @@ function getFusionViewports() { name: DEFAULT_COLORMAP, opacity: [ { value: 0, opacity: 0 }, - { value: 0.1, opacity: 0.3 }, - { value: 1, opacity: 0.3 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.8 }, ], }, }; @@ -191,6 +191,15 @@ function getFusionViewports() { syncInvertState: false, }, }, + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, ], }, displaySets: [ @@ -240,6 +249,15 @@ function getFusionViewports() { syncInvertState: false, }, }, + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, ], }, displaySets: [ @@ -289,6 +307,15 @@ function getFusionViewports() { syncInvertState: false, }, }, + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, ], }, displaySets: [ diff --git a/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx b/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx index f0fddf19a..c4662df7d 100644 --- a/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx +++ b/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { DynamicDataPanel } from './panels'; import { Toolbox } from '@ohif/ui-next'; +import { PanelSegmentation } from '@ohif/extension-cornerstone'; import DynamicExport from './panels/DynamicExport'; -function getPanelModule({ commandsManager, extensionManager, servicesManager }) { - const wrappedDynamicDataPanel = ({}) => { +function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) { + const wrappedDynamicDataPanel = () => { return ( { + const wrappedDynamicSegmentation = () => { return ( <> - - ); - }; - - const wrappedDynamicExport = () => { - return ( - <> - + configuration={configuration} + > + + ); }; @@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }) component: wrappedDynamicDataPanel, }, { - name: 'dynamic-toolbox', - iconName: 'tab-4d', - iconLabel: '4D Workflow', - label: 'Dynamic Toolbox', - component: wrappedDynamicToolbox, - }, - { - name: 'dynamic-export', - iconName: 'tab-4d', - iconLabel: '4D Workflow', - label: '4D Workflow', - component: wrappedDynamicExport, + name: 'dynamic-segmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedDynamicSegmentation, }, ]; } diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx index 6fa2968ed..f3f7a4db9 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx @@ -1,6 +1,5 @@ import React from 'react'; import PanelGenerateImage from './PanelGenerateImage'; -import { Separator } from '@ohif/ui-next'; function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) { return ( diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx index 4a3eb7928..9e8e4a33d 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx @@ -1,73 +1,52 @@ -import React, { useState, useEffect } from 'react'; -import { ActionButtons } from '@ohif/ui'; -import { useTranslation } from 'react-i18next'; +import React from 'react'; +import { Button, Icons } from '@ohif/ui-next'; +import { useSegmentations } from '@ohif/extension-cornerstone'; -function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) { - const { segmentationService } = servicesManager.services; - const { t } = useTranslation('dynamicExport'); +function DynamicExport({ commandsManager, servicesManager }: withAppTypes) { + const segmentations = useSegmentations({ servicesManager }); - const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); - - const actions = [ - { - label: 'Export Time Data', - onClick: () => { - commandsManager.runCommand('exportTimeReportCSV', { - segmentations, - options: { - filename: 'TimeData.csv', - }, - }); - }, - disabled: !segmentations?.length, - }, - { - label: 'Export ROI Stats', - onClick: () => { - commandsManager.runCommand('exportTimeReportCSV', { - segmentations, - summaryStats: true, - options: { - filename: 'ROIStats.csv', - }, - }); - }, - disabled: !segmentations?.length, - }, - ]; - - /** - * Update UI based on segmentation changes (added, removed, updated) - */ - useEffect(() => { - // ~~ Subscription - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; - const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED; - const subscriptions = []; - - [added, updated, removed].forEach(evt => { - const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); - setSegmentations(segmentations); - }); - subscriptions.push(unsubscribe); - }); - - return () => { - subscriptions.forEach(unsub => { - unsub(); - }); - }; - }, []); + if (!segmentations?.length) { + return null; + } return ( -
-
- +
+
+ +
+
+
); diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx index 8a72dd593..00eac97da 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { - InputDoubleRange, Button, PanelSection, ButtonGroup, @@ -10,6 +9,8 @@ import { Tooltip, } from '@ohif/ui'; +import { DoubleSlider } from '@ohif/ui-next'; + import { Enums } from '@cornerstonejs/core'; const controlClassNames = { @@ -55,21 +56,15 @@ const DynamicVolumeControls = ({ const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM); - const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]); - - useEffect(() => { - setSliderRangeValues([framesLength / 4, framesLength / 2]); - }, [framesLength]); + const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]); const handleSliderChange = newValues => { onDoubleRangeChange(newValues); - - if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) { - return; - } setSliderRangeValues(newValues); }; + const formatLabel = value => Math.round(value); + return (
); +
+ ); } /** diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx index cd219b20e..6edc50665 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Enums, VolumeViewport3D } from '@cornerstonejs/core'; -import { utilities as csToolsUtils } from '@cornerstonejs/tools'; +import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core'; import { ImageScrollbar } from '@ohif/ui'; function CornerstoneImageScrollbar({ @@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({ setImageSliceData, scrollbarHeight, servicesManager, -}: withAppTypes) { +}: withAppTypes<{ + element: HTMLElement; +}>) { const { cineService, cornerstoneViewportService } = servicesManager.services; const onImageScrollbarChange = (imageIndex, viewportId) => { @@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({ cineService.setCine({ id: viewportId, isPlaying: false }); } - csToolsUtils.jumpToSlice(viewport.element, { + csUtils.jumpToSlice(viewport.element, { imageIndex, debounceLoading: true, }); @@ -64,7 +65,9 @@ function CornerstoneImageScrollbar({ const updateIndex = event => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if(!viewport || viewport instanceof VolumeViewport3D) return + if (!viewport || viewport instanceof VolumeViewport3D) { + return; + } const { imageIndex, newImageIdIndex = imageIndex } = event.detail; const numberOfSlices = viewport.getNumberOfSlices(); // find the index of imageId in the imageIds diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx index 178705f40..69be2be75 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx @@ -1,14 +1,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import classNames from 'classnames'; -import { - metaData, - Enums, - Types, - getEnabledElement, - utilities as csUtils, -} from '@cornerstonejs/core'; +import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/tools'; -import PropTypes from 'prop-types'; import { vec3 } from 'gl-matrix'; import './ViewportOrientationMarkers.css'; @@ -31,8 +24,9 @@ function ViewportOrientationMarkers({ useEffect(() => { const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => { - const { rotation, previousCamera, camera } = evt.detail; + const { previousCamera, camera } = evt.detail; + const { rotation } = camera; if (rotation !== undefined) { setRotation(rotation); } @@ -74,7 +68,12 @@ function ViewportOrientationMarkers({ return false; } - ({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {}); + ({ + rowCosines, + columnCosines, + isDefaultValueSetForColumnCosine, + isDefaultValueSetForColumnCosine, + } = metaData.get('imagePlaneModule', imageId) || {}); } else { if (!element || !getEnabledElement(element)) { return ''; @@ -90,7 +89,13 @@ function ViewportOrientationMarkers({ rowCosines = viewRight; } - if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) { + if ( + !rowCosines || + !columnCosines || + rotation === undefined || + isDefaultValueSetForRowCosine || + isDefaultValueSetForColumnCosine + ) { return ''; } diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 69f51168a..e4d1c2e67 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -12,15 +12,23 @@ import { utilities as cstUtils, ReferenceLinesTool, } from '@cornerstonejs/tools'; + import { Types as OhifTypes } from '@ohif/core'; +import { + callLabelAutocompleteDialog, + showLabelAnnotationPopup, + createReportAsync, + callInputDialog, + colorPickerDialog, +} from '@ohif/extension-default'; import { vec3, mat4 } from 'gl-matrix'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; -import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog'; import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import toggleVOISliceSync from './utils/toggleVOISliceSync'; +import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores'; const toggleSyncFunctions = { imageSlice: toggleImageSliceSync, @@ -29,6 +37,7 @@ const toggleSyncFunctions = { function commandsModule({ servicesManager, + extensionManager, commandsManager, }: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { const { @@ -101,7 +110,49 @@ function commandsModule({ commandsManager.run(options, optionsToUse); }, + updateStoredSegmentationPresentation: ({ displaySet, type }) => { + const { setSegmentationPresentation } = useSegmentationPresentationStore.getState(); + const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID; + setSegmentationPresentation(referencedDisplaySetInstanceUID, [ + { + segmentationId: displaySet.displaySetInstanceUID, + hydrated: true, + type, + }, + ]); + }, + updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUID }) => { + const presentations = cornerstoneViewportService.getPresentations(viewportId); + const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } = + usePositionPresentationStore.getState(); + + // Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID + // and the value has viewportId as activeViewportId. + const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find( + ([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId + )?.[0]; + + if (previousReferencedDisplaySetStoreKey) { + setPositionPresentation( + previousReferencedDisplaySetStoreKey, + presentations.positionPresentation + ); + + return; + } + + // if not found means we have not visited that referencedDisplaySetInstanceUID before + // so we need to grab the positionPresentationId directly from the store, + // Todo: this is really hacky, we should have a better way for this + + const positionPresentationId = getPositionPresentationId({ + displaySetInstanceUIDs: [displaySetInstanceUID], + viewportId, + }); + + setPositionPresentation(positionPresentationId, presentations.positionPresentation); + }, getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) { return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates); }, @@ -435,9 +486,10 @@ function commandsModule({ viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 }); viewport.render(); } else if (viewport.getRotation !== undefined) { - const currentRotation = viewport.getRotation(); + const presentation = viewport.getViewPresentation(); + const { rotation: currentRotation } = presentation; const newRotation = (currentRotation + rotation) % 360; - viewport.setProperties({ rotation: newRotation }); + viewport.setViewPresentation({ rotation: newRotation }); viewport.render(); } }, @@ -554,7 +606,7 @@ function commandsModule({ // Set slice to last slice const options = { imageIndex: jumpIndex }; - cstUtils.jumpToSlice(viewport.element, options); + csUtils.jumpToSlice(viewport.element, options); }, scroll: ({ direction }) => { const enabledElement = _getActiveViewportEnabledElement(); @@ -566,7 +618,7 @@ function commandsModule({ const { viewport } = enabledElement; const options = { delta: direction }; - cstUtils.scroll(viewport, options); + csUtils.scroll(viewport, options); }, setViewportColormap: ({ viewportId, @@ -577,7 +629,6 @@ function commandsModule({ }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - const actorEntries = viewport.getActors(); let hpOpacity; // Retrieve active protocol's viewport match details const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol(); @@ -596,14 +647,8 @@ function commandsModule({ // HP takes priority over the default opacity colormap = { ...colormap, opacity: hpOpacity || opacity }; - const setViewportProperties = (viewport, uid) => { - const actorEntry = actorEntries.find(entry => entry.uid.includes(uid)); - const { actor: volumeActor, uid: volumeId } = actorEntry; - viewport.setProperties({ colormap, volumeActor }, volumeId); - }; - if (viewport instanceof StackViewport) { - setViewportProperties(viewport, viewportId); + viewport.setProperties({ colormap }); } if (viewport instanceof VolumeViewport) { @@ -611,7 +656,9 @@ function commandsModule({ const { viewports } = viewportGridService.getState(); displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0]; } - setViewportProperties(viewport, displaySetInstanceUID); + + const volumeId = viewport.getVolumeId(); + viewport.setProperties({ colormap }, volumeId); } if (immediate) { @@ -827,6 +874,339 @@ function commandsModule({ ins?.resetCrosshairs(); }); }, + /** + * Creates a labelmap for the active viewport + */ + createLabelmapForViewport: async ({ viewportId, options = {} }) => { + const { viewportGridService, displaySetService, segmentationService } = + servicesManager.services; + const { viewports } = viewportGridService.getState(); + const targetViewportId = viewportId; + + const viewport = viewports.get(targetViewportId); + + // Todo: add support for multiple display sets + const displaySetInstanceUID = + options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0]; + + const segs = segmentationService.getSegmentations(); + + const label = options.label || `Segmentation ${segs.length + 1}`; + const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`; + + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet( + displaySet, + { + label, + segmentationId, + segments: options.createInitialSegment + ? { + 1: { + label: 'Segment 1', + active: true, + }, + } + : {}, + } + ); + + await segmentationService.addSegmentationRepresentation(viewportId, { + segmentationId, + type: Enums.SegmentationRepresentations.Labelmap, + }); + + return generatedSegmentationId; + }, + + /** + * Sets the active segmentation for a viewport + * @param props.segmentationId - The ID of the segmentation to set as active + */ + setActiveSegmentation: ({ segmentationId }) => { + const { viewportGridService, segmentationService } = servicesManager.services; + segmentationService.setActiveSegmentation( + viewportGridService.getActiveViewportId(), + segmentationId + ); + }, + + /** + * Adds a new segment to a segmentation + * @param props.segmentationId - The ID of the segmentation to add the segment to + */ + addSegmentCommand: ({ segmentationId }) => { + const { segmentationService } = servicesManager.services; + segmentationService.addSegment(segmentationId); + }, + + /** + * Sets the active segment and jumps to its center + * @param props.segmentationId - The ID of the segmentation + * @param props.segmentIndex - The index of the segment to activate + */ + setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setActiveSegment(segmentationId, segmentIndex); + segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex); + }, + + /** + * Toggles the visibility of a segment + * @param props.segmentationId - The ID of the segmentation + * @param props.segmentIndex - The index of the segment + * @param props.type - The type of visibility to toggle + */ + toggleSegmentVisibilityCommand: ({ segmentationId, segmentIndex, type }) => { + const { segmentationService, viewportGridService } = servicesManager.services; + segmentationService.toggleSegmentVisibility( + viewportGridService.getActiveViewportId(), + segmentationId, + segmentIndex, + type + ); + }, + + /** + * Toggles the lock state of a segment + * @param props.segmentationId - The ID of the segmentation + * @param props.segmentIndex - The index of the segment + */ + toggleSegmentLockCommand: ({ segmentationId, segmentIndex }) => { + const { segmentationService } = servicesManager.services; + segmentationService.toggleSegmentLocked(segmentationId, segmentIndex); + }, + + /** + * Toggles the visibility of a segmentation representation + * @param props.segmentationId - The ID of the segmentation + * @param props.type - The type of representation + */ + toggleSegmentationVisibilityCommand: ({ segmentationId, type }) => { + const { segmentationService, viewportGridService } = servicesManager.services; + segmentationService.toggleSegmentationRepresentationVisibility( + viewportGridService.getActiveViewportId(), + { segmentationId, type } + ); + }, + + /** + * Downloads a segmentation + * @param props.segmentationId - The ID of the segmentation to download + */ + downloadSegmentationCommand: ({ segmentationId }) => { + const { segmentationService } = servicesManager.services; + segmentationService.downloadSegmentation(segmentationId); + }, + + /** + * Stores a segmentation and shows it in the viewport + * @param props.segmentationId - The ID of the segmentation to store + */ + storeSegmentationCommand: async ({ segmentationId }) => { + const { segmentationService, viewportGridService } = servicesManager.services; + const datasources = extensionManager.getActiveDataSource(); + + const displaySetInstanceUIDs = await createReportAsync({ + servicesManager, + getReport: () => + commandsManager.runCommand('storeSegmentation', { + segmentationId, + dataSource: datasources[0], + }), + reportType: 'Segmentation', + }); + + if (displaySetInstanceUIDs) { + segmentationService.remove(segmentationId); + viewportGridService.setDisplaySetsForViewport({ + viewportId: viewportGridService.getActiveViewportId(), + displaySetInstanceUIDs, + }); + } + }, + + /** + * Downloads a segmentation as RTSS + * @param props.segmentationId - The ID of the segmentation + */ + downloadRTSSCommand: ({ segmentationId }) => { + const { segmentationService } = servicesManager.services; + segmentationService.downloadRTSS(segmentationId); + }, + + /** + * Sets the style for a segmentation + * @param props.segmentationId - The ID of the segmentation + * @param props.type - The type of style + * @param props.key - The style key to set + * @param props.value - The style value + */ + setSegmentationStyleCommand: ({ segmentationId, type, key, value }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setStyle({ type }, { [key]: value }); + }, + + /** + * Deletes a segment from a segmentation + * @param props.segmentationId - The ID of the segmentation + * @param props.segmentIndex - The index of the segment to delete + */ + deleteSegmentCommand: ({ segmentationId, segmentIndex }) => { + const { segmentationService } = servicesManager.services; + segmentationService.removeSegment(segmentationId, segmentIndex); + }, + + /** + * Deletes an entire segmentation + * @param props.segmentationId - The ID of the segmentation to delete + */ + deleteSegmentationCommand: ({ segmentationId }) => { + const { segmentationService } = servicesManager.services; + segmentationService.remove(segmentationId); + }, + + /** + * Removes a segmentation from the viewport + * @param props.segmentationId - The ID of the segmentation to remove + */ + removeSegmentationFromViewportCommand: ({ segmentationId }) => { + const { segmentationService, viewportGridService } = servicesManager.services; + segmentationService.removeSegmentationRepresentations( + viewportGridService.getActiveViewportId(), + { segmentationId } + ); + }, + + /** + * Toggles rendering of inactive segmentations + */ + toggleRenderInactiveSegmentationsCommand: () => { + const { segmentationService, viewportGridService } = servicesManager.services; + const viewportId = viewportGridService.getActiveViewportId(); + const renderInactive = segmentationService.getRenderInactiveSegmentations(viewportId); + segmentationService.setRenderInactiveSegmentations(viewportId, !renderInactive); + }, + + /** + * Sets the fill alpha value for a segmentation type + * @param props.type - The type of segmentation + * @param props.value - The alpha value to set + */ + setFillAlphaCommand: ({ type, value }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setStyle({ type }, { fillAlpha: value }); + }, + + /** + * Sets the outline width for a segmentation type + * @param props.type - The type of segmentation + * @param props.value - The width value to set + */ + setOutlineWidthCommand: ({ type, value }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setStyle({ type }, { outlineWidth: value }); + }, + + /** + * Sets whether to render fill for a segmentation type + * @param props.type - The type of segmentation + * @param props.value - Whether to render fill + */ + setRenderFillCommand: ({ type, value }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setStyle({ type }, { renderFill: value }); + }, + + /** + * Sets whether to render outline for a segmentation type + * @param props.type - The type of segmentation + * @param props.value - Whether to render outline + */ + setRenderOutlineCommand: ({ type, value }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setStyle({ type }, { renderOutline: value }); + }, + + /** + * Sets the fill alpha for inactive segmentations + * @param props.type - The type of segmentation + * @param props.value - The alpha value to set + */ + setFillAlphaInactiveCommand: ({ type, value }) => { + const { segmentationService } = servicesManager.services; + segmentationService.setStyle({ type }, { fillAlphaInactive: value }); + }, + + editSegmentLabel: ({ segmentationId, segmentIndex }) => { + const { segmentationService, uiDialogService } = servicesManager.services; + const segmentation = segmentationService.getSegmentation(segmentationId); + + if (!segmentation) { + return; + } + + const segment = segmentation.segments[segmentIndex]; + const { label } = segment; + + callInputDialog(uiDialogService, label, (label, actionId) => { + if (label === '') { + return; + } + + segmentationService.setSegmentLabel(segmentationId, segmentIndex, label); + }); + }, + + editSegmentationLabel: ({ segmentationId }) => { + const { segmentationService, uiDialogService } = servicesManager.services; + const segmentation = segmentationService.getSegmentation(segmentationId); + + if (!segmentation) { + return; + } + + const { label } = segmentation; + + callInputDialog(uiDialogService, label, (label, actionId) => { + if (label === '') { + return; + } + + segmentationService.addOrUpdateSegmentation({ segmentationId, label }); + }); + }, + + editSegmentColor: ({ segmentationId, segmentIndex }) => { + const { segmentationService, uiDialogService, viewportGridService } = + servicesManager.services; + const viewportId = viewportGridService.getActiveViewportId(); + const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex); + + const rgbaColor = { + r: color[0], + g: color[1], + b: color[2], + a: color[3] / 255.0, + }; + + colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { + if (actionId === 'cancel') { + return; + } + + const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; + segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); + }); + }, + + getRenderInactiveSegmentations: () => { + const { segmentationService, viewportGridService } = servicesManager.services; + return segmentationService.getRenderInactiveSegmentations( + viewportGridService.getActiveViewportId() + ); + }, }; const definitions = { @@ -988,6 +1368,84 @@ function commandsModule({ toggleActiveDisabledToolbar: { commandFn: actions.toggleActiveDisabledToolbar, }, + updateStoredPositionPresentation: { + commandFn: actions.updateStoredPositionPresentation, + }, + updateStoredSegmentationPresentation: { + commandFn: actions.updateStoredSegmentationPresentation, + }, + createLabelmapForViewport: { + commandFn: actions.createLabelmapForViewport, + }, + setActiveSegmentation: { + commandFn: actions.setActiveSegmentation, + }, + addSegment: { + commandFn: actions.addSegmentCommand, + }, + setActiveSegmentAndCenter: { + commandFn: actions.setActiveSegmentAndCenterCommand, + }, + toggleSegmentVisibility: { + commandFn: actions.toggleSegmentVisibilityCommand, + }, + toggleSegmentLock: { + commandFn: actions.toggleSegmentLockCommand, + }, + toggleSegmentationVisibility: { + commandFn: actions.toggleSegmentationVisibilityCommand, + }, + downloadSegmentation: { + commandFn: actions.downloadSegmentationCommand, + }, + storeSegmentation: { + commandFn: actions.storeSegmentationCommand, + }, + downloadRTSS: { + commandFn: actions.downloadRTSSCommand, + }, + setSegmentationStyle: { + commandFn: actions.setSegmentationStyleCommand, + }, + deleteSegment: { + commandFn: actions.deleteSegmentCommand, + }, + deleteSegmentation: { + commandFn: actions.deleteSegmentationCommand, + }, + removeSegmentationFromViewport: { + commandFn: actions.removeSegmentationFromViewportCommand, + }, + toggleRenderInactiveSegmentations: { + commandFn: actions.toggleRenderInactiveSegmentationsCommand, + }, + setFillAlpha: { + commandFn: actions.setFillAlphaCommand, + }, + setOutlineWidth: { + commandFn: actions.setOutlineWidthCommand, + }, + setRenderFill: { + commandFn: actions.setRenderFillCommand, + }, + setRenderOutline: { + commandFn: actions.setRenderOutlineCommand, + }, + setFillAlphaInactive: { + commandFn: actions.setFillAlphaInactiveCommand, + }, + editSegmentLabel: { + commandFn: actions.editSegmentLabel, + }, + editSegmentationLabel: { + commandFn: actions.editSegmentationLabel, + }, + editSegmentColor: { + commandFn: actions.editSegmentColor, + }, + getRenderInactiveSegmentations: { + commandFn: actions.getRenderInactiveSegmentations, + }, }; return { diff --git a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx index af78066ab..06b292e3c 100644 --- a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx +++ b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useState, useRef } from 'react'; import { CinePlayer, useCine } from '@ohif/ui'; import { Enums, eventTarget, cache } from '@cornerstonejs/core'; -import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader'; import { useAppConfig } from '@state'; function WrappedCinePlayer({ @@ -101,7 +100,7 @@ function WrappedCinePlayer({ return; } - eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler); + enabledVPElement.addEventListener(Enums.Events.VIEWPORT_NEW_IMAGE_SET, newDisplaySetHandler); // this doesn't makes sense that we are listening to this event on viewport element enabledVPElement.addEventListener( Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, @@ -111,7 +110,10 @@ function WrappedCinePlayer({ return () => { cineService.setCine({ id: viewportId, isPlaying: false }); - eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler); + enabledVPElement.removeEventListener( + Enums.Events.VIEWPORT_NEW_IMAGE_SET, + newDisplaySetHandler + ); enabledVPElement.removeEventListener( Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, newDisplaySetHandler @@ -181,13 +183,13 @@ function RenderCinePlayer({ }; eventTarget.addEventListener( - StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED, + Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED, handleTimePointIndexChange ); return () => { eventTarget.removeEventListener( - StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED, + Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED, handleTimePointIndexChange ); }; @@ -199,7 +201,7 @@ function RenderCinePlayer({ } const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {}; - const volume = cache.getVolume(volumeId); + const volume = cache.getVolume(volumeId, true); volume.timePointIndex = timePointIndex; setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag }); @@ -207,7 +209,7 @@ function RenderCinePlayer({ const updateDynamicInfo = useCallback(props => { const { volumeId, timePointIndex } = props; - const volume = cache.getVolume(volumeId); + const volume = cache.getVolume(volumeId, true); volume.timePointIndex = timePointIndex; }, []); @@ -223,6 +225,7 @@ function RenderCinePlayer({ isPlaying: false, }); cineService.setIsCineEnabled(false); + cineService.setViewportCineClosed(viewportId); }} onPlayPauseChange={isPlaying => { cineService.setCine({ diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx new file mode 100644 index 000000000..93f9c5864 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Icons, Separator } from '@ohif/ui-next'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; + +function ViewportSegmentationMenu({ + viewportId, + servicesManager, +}: withAppTypes<{ viewportId: string }>) { + const { segmentationService } = servicesManager.services; + const [activeSegmentations, setActiveSegmentations] = useState([]); + const [availableSegmentations, setAvailableSegmentations] = useState([]); + + useEffect(() => { + const updateSegmentations = () => { + const active = segmentationService.getSegmentationRepresentations(viewportId); + setActiveSegmentations(active); + + const all = segmentationService.getSegmentations(); + const available = all.filter( + seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId) + ); + setAvailableSegmentations(available); + }; + + updateSegmentations(); + + const subscriptions = [ + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + segmentationService.EVENTS.SEGMENTATION_REMOVED, + segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, + ].map(event => segmentationService.subscribe(event, updateSegmentations)); + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + }; + }, [segmentationService, viewportId]); + + const toggleSegmentationRepresentationVisibility = ( + segmentationId, + type = SegmentationRepresentations.Labelmap + ) => { + segmentationService.toggleSegmentationRepresentationVisibility(viewportId, { + segmentationId, + type, + }); + }; + + const addSegmentationToViewport = segmentationId => { + segmentationService.addSegmentationRepresentation(viewportId, { segmentationId }); + }; + + const removeSegmentationFromViewport = segmentationId => { + segmentationService.removeSegmentationRepresentations(viewportId, { + segmentationId, + }); + }; + + return ( +
+ Current Viewport +
    + {activeSegmentations.map(segmentation => ( +
  • + + {segmentation.label} + {segmentation.visible ? ( + + ) : ( + + )} +
  • + ))} +
+ {availableSegmentations.length > 0 && ( + <> + + Available +
    + {availableSegmentations.map(segmentation => ( +
  • + + {segmentation.segmentation.label} +
  • + ))} +
+ + )} +
+ ); +} + +export default ViewportSegmentationMenu; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx new file mode 100644 index 000000000..8fcd5cf34 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx @@ -0,0 +1,101 @@ +import React, { ReactNode, useEffect, useState } from 'react'; +import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next'; +import ViewportSegmentationMenu from './ViewportSegmentationMenu'; +import classNames from 'classnames'; + +export function ViewportSegmentationMenuWrapper({ + viewportId, + displaySets, + servicesManager, + commandsManager, + location, +}: withAppTypes<{ + viewportId: string; + element: HTMLElement; +}>): ReactNode { + const { segmentationService, viewportActionCornersService, viewportGridService } = + servicesManager.services; + const [representations, setRepresentations] = useState(null); + + useEffect(() => { + const representations = segmentationService.getSegmentationRepresentations(viewportId); + setRepresentations(representations); + }, [viewportId, segmentationService]); + + useEffect(() => { + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, + () => { + const representations = segmentationService.getSegmentationRepresentations(viewportId); + setRepresentations(representations); + } + ); + + return () => { + unsubscribe(); + }; + }, [viewportId, segmentationService]); + + const activeViewportId = viewportGridService.getActiveViewportId(); + const isActiveViewport = viewportId === activeViewportId; + + const { align, side } = getAlignAndSide(viewportActionCornersService, location); + + if (!representations?.length) { + return null; + } + + return ( + + + + + + + + + ); +} + +const getAlignAndSide = (viewportActionCornersService, location) => { + const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS; + + switch (location) { + case ViewportActionCornersLocations.topLeft: + return { align: 'start', side: 'bottom' }; + case ViewportActionCornersLocations.topRight: + return { align: 'end', side: 'bottom' }; + case ViewportActionCornersLocations.bottomLeft: + return { align: 'start', side: 'top' }; + case ViewportActionCornersLocations.bottomRight: + return { align: 'end', side: 'top' }; + default: + console.debug('Unknown location, defaulting to bottom-start'); + return { align: 'start', side: 'bottom' }; + } +}; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx new file mode 100644 index 000000000..5933a82cd --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode } from 'react'; +import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper'; + +export function getViewportDataOverlaySettingsMenu( + props: withAppTypes<{ + viewportId: string; + element: HTMLElement; + }> +): ReactNode { + return ; +} diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx index 522022965..3810d5467 100644 --- a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx @@ -1,10 +1,15 @@ -import React, { useEffect, useCallback, useState, ReactElement } from 'react'; +import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react'; import PropTypes from 'prop-types'; import debounce from 'lodash.debounce'; import { PanelSection, WindowLevel } from '@ohif/ui'; -import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; -import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core'; -import { getViewportVolumeHistogram } from './getViewportVolumeHistogram'; +import { Enums, eventTarget } from '@cornerstonejs/core'; +import { useActiveViewportDisplaySets } from '@ohif/core'; +import { + getNodeOpacity, + isPetVolumeWithDefaultOpacity, + isVolumeWithConstantOpacity, + getWindowLevelsData, +} from './utils'; const { Events } = Enums; @@ -16,19 +21,16 @@ const ViewportWindowLevel = ({ }>): ReactElement => { const { cornerstoneViewportService } = servicesManager.services; const [windowLevels, setWindowLevels] = useState([]); - const [cachedHistograms, setCachedHistograms] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const displaySets = useActiveViewportDisplaySets({ servicesManager }); - /** - * Looks for all viewports that has exactly all volumeIds passed as parameter. - */ const getViewportsWithVolumeIds = useCallback( (volumeIds: string[]) => { const renderingEngine = cornerstoneViewportService.getRenderingEngine(); const viewports = renderingEngine.getVolumeViewports(); return viewports.filter(vp => { - const viewportVolumeIds = vp.getActors().map(actor => actor.uid); - + const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId); return ( volumeIds.length === viewportVolumeIds.length && volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId)) @@ -38,97 +40,10 @@ const ViewportWindowLevel = ({ [cornerstoneViewportService] ); - const getNodeOpacity = (volumeActor, nodeIndex) => { - const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); - const nodeValue = []; - - volumeOpacity.getNodeValue(nodeIndex, nodeValue); - - return nodeValue[1]; - }; - - /** - * Checks if the opacity applied to the PET volume is something like - * [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a - * constant opacity value for all x's greater than 0.1 - */ - const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => { - const volume = cs3DCache.getVolume(volumeId); - - if (!volume) { - return false; - } - - const modality = volume.metadata.Modality; - - if (modality !== 'PT') { - return false; - } - - const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); - - // It must have at least two points (0 and 0.1) - if (volumeOpacity.getSize() < 2) { - return false; - } - - const node1Value = []; - const node2Value = []; - - volumeOpacity.getNodeValue(0, node1Value); - volumeOpacity.getNodeValue(1, node2Value); - - // First node must be (x:0, y:0} and the second one {x:0.1, y:any} - if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) { - return false; - } - - const expectedOpacity = node2Value[1]; - const opacitySize = volumeOpacity.getSize(); - const currentNodeValue = []; - - // Any point after 0.1 must have the same opacity - for (let i = 2; i < opacitySize; i++) { - volumeOpacity.getNodeValue(i, currentNodeValue); - - if (currentNodeValue[1] !== expectedOpacity) { - return false; - } - } - - return true; - }; - - /** - * Checks if the opacity function has a constance opacity value for all x's - */ - const isVolumeWithConstantOpacity = volumeActor => { - const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); - const opacitySize = volumeOpacity.getSize(); - const firstNodeValue = []; - - volumeOpacity.getNodeValue(0, firstNodeValue); - - const firstNodeOpacity = firstNodeValue[1]; - - for (let i = 0; i < opacitySize; i++) { - const currentNodeValue = []; - - volumeOpacity.getNodeValue(0, currentNodeValue); - - if (currentNodeValue[1] !== firstNodeOpacity) { - return false; - } - } - - return true; - }; - const getVolumeOpacity = useCallback((viewport, volumeId) => { - const volumeActor = viewport.getActor(volumeId).actor; + const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor; if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) { - // Get the opacity from the second node at 0.1 return getNodeOpacity(volumeActor, 1); } else if (isVolumeWithConstantOpacity(volumeActor)) { return getNodeOpacity(volumeActor, 0); @@ -137,85 +52,14 @@ const ViewportWindowLevel = ({ return undefined; }, []); - const getWindowLevelsData = useCallback( - async (viewportId: number) => { - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - - if (!viewport) { - return []; - } - - const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); - - const volumeIds = viewport.getActors().map(actor => actor.uid); - const viewportProperties = viewport.getProperties(); - const { voiRange } = viewportProperties; - const viewportVoi = voiRange - ? { - windowWidth: voiRange.upper - voiRange.lower, - windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2, - } - : undefined; - - const windowLevels = await Promise.all( - volumeIds.map(async (volumeId, volumeIndex) => { - const volume = cs3DCache.getVolume(volumeId); - - if (!volume) { - return null; - } - - const opacity = getVolumeOpacity(viewport, volumeId); - const { metadata, scaling } = volume; - const modality = metadata.Modality; - - // TODO: find a proper way to fix the histogram - const options = { - min: modality === 'PT' ? 0.1 : -999, - max: modality === 'PT' ? 5 : 10000, - }; - - const histogram = - cachedHistograms[volumeId] ?? - (await getViewportVolumeHistogram(viewport, volume, options)); - const { voi: displaySetVOI, colormap: displaySetColormap } = - viewportInfo.displaySetOptions[volumeIndex]; - let colormap; - if (displaySetColormap) { - colormap = - csUtils.colormap.getColormap(displaySetColormap.name) ?? - vtkColorMaps.getPresetByName(displaySetColormap.name); - } - - const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI; - - return { - viewportId, - modality, - volumeId, - volumeIndex, - voi, - histogram, - colormap, - step: scaling?.PT ? 0.05 : 1, - opacity, - showOpacitySlider: volumeIndex === 1 && opacity !== undefined, - }; - }) - ); - - const data = windowLevels.filter(Boolean); - return data; - }, - - [cachedHistograms, cornerstoneViewportService, getVolumeOpacity] - ); - const updateViewportHistograms = useCallback(() => { - getWindowLevelsData(viewportId).then(windowLevels => { - setWindowLevels(windowLevels); + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + + getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => { + setWindowLevels(data); }); - }, [viewportId, getWindowLevelsData]); + }, [viewportId, cornerstoneViewportService, getVolumeOpacity]); const handleCornerstoneVOIModified = useCallback( e => { @@ -252,8 +96,8 @@ const ViewportWindowLevel = ({ [windowLevels] ); - const debouncedHandleCornerstoneVOIModified = useCallback( - debounce(handleCornerstoneVOIModified, 100), + const debouncedHandleCornerstoneVOIModified = useMemo( + () => debounce(handleCornerstoneVOIModified, 100), [handleCornerstoneVOIModified] ); @@ -280,7 +124,7 @@ const ViewportWindowLevel = ({ return; } - const viewportVolumeIds = viewport.getActors().map(actor => actor.uid); + const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId); const viewports = getViewportsWithVolumeIds(viewportVolumeIds); viewports.forEach(vp => { @@ -291,49 +135,50 @@ const ViewportWindowLevel = ({ [getViewportsWithVolumeIds, cornerstoneViewportService] ); - // Listen to windowLevels changes and caches all the new ones + // New function to handle image volume loading completion + const handleImageVolumeLoadingCompleted = useCallback(() => { + setIsLoading(false); + updateViewportHistograms(); + }, [updateViewportHistograms]); + + // Listen to cornerstone events and set up interval for histogram updates useEffect(() => { - const newVolumeHistograms = windowLevels - .filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram) - .reduce((volumeHistograms, windowLevel) => { - // If the histogram exists, add it to the volumeHistograms object - if (windowLevel?.histogram) { - volumeHistograms[windowLevel.volumeId] = windowLevel.histogram; - } - return volumeHistograms; - }, {}); - - if (Object.keys(newVolumeHistograms).length) { - setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms })); - } - }, [windowLevels, cachedHistograms]); - - // Updates the histogram when the viewport index prop has changed - useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]); - - // Listen to cornerstone events on "eventTarget" and at the document level - useEffect(() => { - eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms); - document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true); + eventTarget.addEventListener( + Events.IMAGE_VOLUME_LOADING_COMPLETED, + handleImageVolumeLoadingCompleted + ); + + const intervalId = setInterval(() => { + if (isLoading) { + updateViewportHistograms(); + } + }, 1000); return () => { - eventTarget.removeEventListener( - Events.IMAGE_VOLUME_LOADING_COMPLETED, - updateViewportHistograms - ); - document.removeEventListener( Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true ); + eventTarget.removeEventListener( + Events.IMAGE_VOLUME_LOADING_COMPLETED, + handleImageVolumeLoadingCompleted + ); + clearInterval(intervalId); }; - }, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]); + }, [ + updateViewportHistograms, + debouncedHandleCornerstoneVOIModified, + handleImageVolumeLoadingCompleted, + isLoading, + ]); + + // Create a memoized version of displaySet IDs for comparison + const displaySetIds = useMemo(() => { + return displaySets?.map(ds => ds.displaySetInstanceUID).sort() || []; + }, [displaySets]); - // Updates the viewport when the context of the viewport has changed. This is - // necessary when moving across different stages because the viewport index - // may not change but the volumes loaded on it may change. useEffect(() => { const { unsubscribe } = cornerstoneViewportService.subscribe( cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED, @@ -344,10 +189,15 @@ const ViewportWindowLevel = ({ } ); + // Only update if displaySets actually changed and are loaded + if (displaySetIds.length && !isLoading) { + updateViewportHistograms(); + } + return () => { unsubscribe(); }; - }, [viewportId, cornerstoneViewportService, updateViewportHistograms]); + }, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]); return ( diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts b/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts index 167d49f8a..f3911e42b 100644 --- a/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts @@ -20,10 +20,6 @@ const workerFn = () => { const getViewportVolumeHistogram = async (viewport, volume, options?) => { workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions); - if (!volume?.loadStatus.loaded) { - return undefined; - } - const volumeImageData = viewport.getImageData(volume.volumeId); if (!volumeImageData) { @@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => { let scalarData = volume.scalarData; - let prevTimePoint; if (volume.numTimePoints > 1) { - prevTimePoint = volume.timePointIndex; - const middleTimePoint = Math.round(volume.numTimePoints / 2); - volume.timePointIndex = middleTimePoint; - scalarData = volume.getScalarData(middleTimePoint); + const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need + scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint); + } else { + scalarData = volume.voxelManager.getCompleteScalarDataArray(); + } + + if (!scalarData?.length) { + return undefined; } const { dimensions, origin, direction, spacing } = volume; @@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => { scalarData, }); - // after we calculate the range let's reset the timePointIndex - if (volume.numTimePoints > 1) { - volume.timePointIndex = prevTimePoint; - } const { minimum: min, maximum: max } = range; + + if (min === Infinity || max === -Infinity) { + return undefined; + } + const calcHistOptions = { numBins: 256, min: Math.max(min, options?.min ?? min), diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts b/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts new file mode 100644 index 000000000..8dcbf509c --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts @@ -0,0 +1,153 @@ +import { cache as cs3DCache, Types } from '@cornerstonejs/core'; +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; +import { utilities as csUtils } from '@cornerstonejs/core'; +import { getViewportVolumeHistogram } from './getViewportVolumeHistogram'; + +/** + * Gets node opacity from volume actor + */ +export const getNodeOpacity = (volumeActor, nodeIndex) => { + const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); + const nodeValue = []; + + volumeOpacity.getNodeValue(nodeIndex, nodeValue); + + return nodeValue[1]; +}; + +/** + * Checks if the opacity applied to the PET volume follows a specific pattern + */ +export const isPetVolumeWithDefaultOpacity = (volumeId: string, volumeActor) => { + const volume = cs3DCache.getVolume(volumeId); + + if (!volume || volume.metadata.Modality !== 'PT') { + return false; + } + + const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); + + if (volumeOpacity.getSize() < 2) { + return false; + } + + const node1Value = []; + const node2Value = []; + + volumeOpacity.getNodeValue(0, node1Value); + volumeOpacity.getNodeValue(1, node2Value); + + if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) { + return false; + } + + const expectedOpacity = node2Value[1]; + const opacitySize = volumeOpacity.getSize(); + const currentNodeValue = []; + + for (let i = 2; i < opacitySize; i++) { + volumeOpacity.getNodeValue(i, currentNodeValue); + if (currentNodeValue[1] !== expectedOpacity) { + return false; + } + } + + return true; +}; + +/** + * Checks if volume has constant opacity + */ +export const isVolumeWithConstantOpacity = volumeActor => { + const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); + const opacitySize = volumeOpacity.getSize(); + const firstNodeValue = []; + + volumeOpacity.getNodeValue(0, firstNodeValue); + const firstNodeOpacity = firstNodeValue[1]; + + for (let i = 0; i < opacitySize; i++) { + const currentNodeValue = []; + volumeOpacity.getNodeValue(0, currentNodeValue); + if (currentNodeValue[1] !== firstNodeOpacity) { + return false; + } + } + + return true; +}; + +/** + * Gets window levels data for a viewport + */ +export const getWindowLevelsData = async ( + viewport: Types.IStackViewport | Types.IVolumeViewport, + viewportInfo: any, + getVolumeOpacity: (viewport: any, volumeId: string) => number | undefined +) => { + if (!viewport) { + return []; + } + + const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds(); + const viewportProperties = viewport.getProperties(); + const { voiRange } = viewportProperties; + const viewportVoi = voiRange + ? { + windowWidth: voiRange.upper - voiRange.lower, + windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2, + } + : undefined; + + const windowLevels = await Promise.all( + volumeIds.map(async (volumeId, volumeIndex) => { + const volume = cs3DCache.getVolume(volumeId); + + const opacity = getVolumeOpacity(viewport, volumeId); + const { metadata, scaling } = volume; + const modality = metadata.Modality; + + const options = { + min: modality === 'PT' ? 0.1 : -999, + max: modality === 'PT' ? 5 : 10000, + }; + + const histogram = await getViewportVolumeHistogram(viewport, volume, options); + + if (!histogram || histogram.range.min === histogram.range.max) { + return null; + } + + if (!viewportInfo.displaySetOptions || !viewportInfo.displaySetOptions[volumeIndex]) { + return null; + } + + const { voi: displaySetVOI, colormap: displaySetColormap } = + viewportInfo.displaySetOptions[volumeIndex]; + + let colormap; + if (displaySetColormap) { + colormap = + csUtils.colormap.getColormap(displaySetColormap.name) ?? + vtkColorMaps.getPresetByName(displaySetColormap.name); + } + + const voi = !volumeIndex ? (viewportVoi ?? displaySetVOI) : displaySetVOI; + + return { + viewportId: viewportInfo.viewportId, + modality, + volumeId, + volumeIndex, + voi, + histogram, + colormap, + step: scaling?.PT ? 0.05 : 1, + opacity, + showOpacitySlider: volumeIndex === 1 && opacity !== undefined, + }; + }) + ); + + return windowLevels.filter(Boolean); +}; diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx index 51764c956..93c2ec481 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react'; import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui'; -import { StackViewport } from '@cornerstonejs/core'; +import { StackViewport, Types } from '@cornerstonejs/core'; import { ColormapProps } from '../../types/Colormap'; export function Colormap({ @@ -54,8 +54,10 @@ export function Colormap({ return colormap; } const actorEntries = viewport.getActors(); - const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID)); - const { colormap } = viewport.getProperties(actorEntry.uid); + const actorEntry = actorEntries?.find(entry => + entry.referencedId.includes(displaySetInstanceUID) + ); + const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId); if (!colormap) { return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0]; } diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx index 55b4d53b9..c4196f9d7 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx @@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets'; import { VolumeViewport3D } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/core'; +export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; + export type WindowLevelActionMenuProps = { viewportId: string; element: HTMLElement; presets: Array>>; - verticalDirection: AllInOneMenu.VerticalDirection; - horizontalDirection: AllInOneMenu.HorizontalDirection; colorbarProperties: ColorbarProperties; displaySets: Array; volumeRenderingPresets: Array; @@ -53,8 +53,6 @@ export function WindowLevelActionMenu({ const backgroundColor = viewportInfo.getViewportOptions().background; const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false; - const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; - const { t } = useTranslation('WindowLevelActionMenu'); const [viewportGrid] = useViewportGrid(); @@ -119,7 +117,7 @@ export function WindowLevelActionMenu({ horizontalDirection={horizontalDirection} iconClassName={classNames( // Visible on hover and for the active viewport - activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible', + activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible', 'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light', isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60' )} @@ -133,7 +131,7 @@ export function WindowLevelActionMenu({ {!is3DVolume && ( !nonImageModalities.includes(ds.Modality))} + displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))} commandsManager={commandsManager} servicesManager={servicesManager} colorbarProperties={colorbarProperties} @@ -149,7 +147,7 @@ export function WindowLevelActionMenu({ !nonImageModalities.includes(ds.Modality))} + displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))} commandsManager={commandsManager} servicesManager={servicesManager} /> diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx index c18176a58..d8d2cbda8 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from 'react'; -import { WindowLevelActionMenu } from './WindowLevelActionMenu'; +import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu'; export function getWindowLevelActionMenu({ viewportId, @@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({ commandsManager, verticalDirection, horizontalDirection, -}: withAppTypes): ReactNode { +}: withAppTypes<{ + viewportId: string; + element: HTMLElement; + displaySets: AppTypes.DisplaySet[]; +}>): ReactNode { const { customizationService } = servicesManager.services; const { presets } = customizationService.get('cornerstone.windowLevelPresets'); @@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({ return { [displaySet.Modality]: presets[displaySet.Modality] }; }); + const modalities = displaySets + .map(displaySet => displaySet.Modality) + .filter(modality => !nonWLModalities.includes(modality)); + + if (modalities.length === 0) { + return null; + } + return ( { switch (action.type) { - case 'SET_ACTION_COMPONENT': { - const { viewportId, id, component, location, indexPriority = 0 } = action.payload; + case 'ADD_ACTION_COMPONENT': { + const { viewportId, id, component, location, indexPriority } = action.payload; // Get the components at the specified location of the specified viewport. let locationComponents = state?.[viewportId]?.[location] ? [...state[viewportId][location]] @@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) { // Insert the component from the payload but // do not insert an undefined or null component. if (component) { - const insertionIndex = locationComponents.findIndex( - component => indexPriority <= component.indexPriority - ); + let insertionIndex; + const isRightSide = + location === ViewportActionCornersLocations.topRight || + location === ViewportActionCornersLocations.bottomRight; + + if (indexPriority === undefined) { + // If no indexPriority is provided, add it to the appropriate end + insertionIndex = isRightSide ? 0 : locationComponents.length; + } else { + if (isRightSide) { + insertionIndex = locationComponents.findIndex( + component => indexPriority > component.indexPriority + ); + } else { + insertionIndex = locationComponents.findIndex( + component => indexPriority <= component.indexPriority + ); + } + if (insertionIndex === -1) { + // If no suitable position found, add to the appropriate end + insertionIndex = isRightSide ? 0 : locationComponents.length; + } + } + + const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; + locationComponents = [ ...locationComponents.slice(0, insertionIndex), { id, component, - indexPriority, + indexPriority: indexPriority ?? defaultPriority, }, - ...locationComponents.slice(insertionIndex + 1), + ...locationComponents.slice(insertionIndex), ]; } return { ...state, - ...{ - [viewportId]: { - ...state[viewportId], - [location]: locationComponents, - }, + [viewportId]: { + ...state[viewportId], + [location]: locationComponents, }, }; } @@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) { return viewportActionCornersState; }, [viewportActionCornersState]); - const setComponent = useCallback( + const addComponent = useCallback( (actionComponentInfo: ActionComponentInfo) => { - dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo }); + dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo }); }, [dispatch] ); - const setComponents = useCallback( + const addComponents = useCallback( (actionComponentInfos: Array) => { actionComponentInfos.forEach(actionComponentInfo => - dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo }) + dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo }) ); }, [dispatch] @@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) { (viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }), [dispatch] ); + useEffect(() => { if (service) { service.setServiceImplementation({ getState, - setComponent, - setComponents, + addComponent, + addComponents, clear, }); } - }, [getState, service, setComponent, setComponents]); + }, [getState, service, addComponent, addComponents, clear]); - // run many of the calls through the service itself since we want to publish events - const api = { + const viewportCornerActions = { getState, - setComponent: props => service.setComponent(props), - setComponents: props => service.setComponents(props), + addComponent: props => service.addComponent(props), + addComponents: props => service.addComponents(props), clear: props => service.clear(props), }; const contextValue = useMemo( - () => [viewportActionCornersState, api], - [viewportActionCornersState, api] + () => [viewportActionCornersState, viewportCornerActions], + [viewportActionCornersState, viewportCornerActions] ); return ( diff --git a/extensions/cornerstone/src/getCustomizationModule.ts b/extensions/cornerstone/src/getCustomizationModule.ts index 02cc7a0f7..cf00156fe 100644 --- a/extensions/cornerstone/src/getCustomizationModule.ts +++ b/extensions/cornerstone/src/getCustomizationModule.ts @@ -1,6 +1,5 @@ import { Enums } from '@cornerstonejs/tools'; import { toolNames } from './initCornerstoneTools'; -import DicomUpload from './components/DicomUpload/DicomUpload'; import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets'; import { colormaps } from './utils/colormaps'; import { CONSTANTS } from '@cornerstonejs/core'; @@ -23,10 +22,12 @@ const tools = { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], enabled: [ - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.PlanarFreehandContourSegmentation, configuration: { @@ -38,13 +39,6 @@ const tools = { function getCustomizationModule() { return [ - { - name: 'cornerstoneDicomUploadComponent', - value: { - id: 'dicomUploadComponent', - component: DicomUpload, - }, - }, { name: 'default', value: [ @@ -120,7 +114,7 @@ function getCustomizationModule() { type: 'value', }, { - value: 'areaUnit', + value: 'areaUnits', for: ['area'], type: 'unit', }, @@ -140,7 +134,7 @@ function getCustomizationModule() { }, { displayName: 'Unit', - value: 'areaUnit', + value: 'areaUnits', type: 'value', }, ], @@ -170,12 +164,12 @@ function getCustomizationModule() { type: 'value', }, { - value: 'modalityUnit', + value: 'pixelValueUnits', for: ['mean', 'max' /** 'stdDev **/], type: 'unit', }, { - value: 'areaUnit', + value: 'areaUnits', for: ['area'], type: 'unit', }, diff --git a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx b/extensions/cornerstone/src/getPanelModule.tsx similarity index 68% rename from extensions/cornerstone-dicom-seg/src/getPanelModule.tsx rename to extensions/cornerstone/src/getPanelModule.tsx index 1a135e152..48cbd6eaa 100644 --- a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx +++ b/extensions/cornerstone/src/getPanelModule.tsx @@ -1,15 +1,11 @@ import React from 'react'; -import { useAppConfig } from '@state'; import { Toolbox } from '@ohif/ui-next'; import PanelSegmentation from './panels/PanelSegmentation'; +import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel'; const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => { - const { customizationService } = servicesManager.services; - const wrappedPanelSegmentation = ({ configuration }) => { - const [appConfig] = useAppConfig(); - return ( + ); + }; + + const wrappedPanelSegmentationNoHeader = ({ configuration }) => { + return ( + ); }; const wrappedPanelSegmentationWithTools = ({ configuration }) => { - const [appConfig] = useAppConfig(); - return ( <> @@ -54,6 +57,12 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: }; return [ + { + name: 'activeViewportWindowLevel', + component: () => { + return ; + }, + }, { name: 'panelSegmentation', iconName: 'tab-segmentation', @@ -61,6 +70,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: label: 'Segmentation', component: wrappedPanelSegmentation, }, + { + name: 'panelSegmentationNoHeader', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentationNoHeader, + }, { name: 'panelSegmentationWithTools', iconName: 'tab-segmentation', diff --git a/extensions/cornerstone/src/getSopClassHandlerModule.js b/extensions/cornerstone/src/getSopClassHandlerModule.js index e3b5fae21..f88dc66ae 100644 --- a/extensions/cornerstone/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone/src/getSopClassHandlerModule.js @@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) const displaySet = { plugin: 'microscopy', Modality: 'SM', - viewportType: csEnums.ViewportType.WholeSlide, + viewportType: csEnums.ViewportType.WHOLE_SLIDE, altImageText: 'Microscopy', displaySetInstanceUID: utils.guid(), SOPInstanceUID, diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx index 6c99065eb..e94eb895f 100644 --- a/extensions/cornerstone/src/getToolbarModule.tsx +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => { : '!text-common-bright hover:!bg-primary-dark hover:text-primary-light'; }; +const getDisabledState = (disabledText?: string) => ({ + disabled: true, + className: '!text-common-bright ohif-disabled', + disabledText: disabledText ?? 'Not available on the current viewport', +}); + export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) { const { toolGroupService, @@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w return [ // functions/helpers to be used by the toolbar buttons to decide if they should // enabled or not + { + name: 'evaluate.viewport.supported', + evaluate: ({ viewportId, unsupportedViewportTypes, disabledText }) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + if (viewport && unsupportedViewportTypes?.includes(viewport.type)) { + return getDisabledState(disabledText); + } + + return undefined; + }, + }, + { + name: 'evaluate.modality.supported', + evaluate: ({ viewportId, unsupportedModalities, supportedModalities, disabledText }) => { + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + if (!displaySetUIDs?.length) { + return; + } + + const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID); + + // Check for unsupported modalities (exclusion) + if (unsupportedModalities?.length) { + const hasUnsupportedModality = displaySets.some(displaySet => + unsupportedModalities.includes(displaySet?.Modality) + ); + + if (hasUnsupportedModality) { + return getDisabledState(disabledText); + } + } + + // Check for supported modalities (inclusion) + if (supportedModalities?.length) { + const hasAnySupportedModality = displaySets.some(displaySet => + supportedModalities.includes(displaySet?.Modality) + ); + + if (!hasAnySupportedModality) { + return getDisabledState(disabledText || 'Tool not available for this modality'); + } + } + }, + }, { name: 'evaluate.cornerstoneTool', evaluate: ({ viewportId, button, toolNames, disabledText }) => { @@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w const toolName = toolbarService.getToolNameForButton(button); if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) { - return { - disabled: true, - className: '!text-common-bright ohif-disabled', - disabledText: disabledText ?? 'Not available on the current viewport', - }; + return getDisabledState(disabledText); } const isPrimaryActive = toolNames @@ -172,40 +220,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w }; }, }, - { - name: 'evaluate.not3D', - evaluate: ({ viewportId, disabledText }) => { - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - - if (viewport?.type === 'volume3d') { - return { - disabled: true, - className: '!text-common-bright ohif-disabled', - disabledText: disabledText ?? 'Not available on the current viewport', - }; - } - }, - }, - { - name: 'evaluate.isUS', - evaluate: ({ viewportId, disabledText }) => { - const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); - - if (!displaySetUIDs?.length) { - return; - } - - const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID); - const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US'); - if (!isUS) { - return { - disabled: true, - className: '!text-common-bright ohif-disabled', - disabledText: disabledText ?? 'Not available on the current viewport', - }; - } - }, - }, { name: 'evaluate.viewportProperties.toggle', evaluate: ({ viewportId, button }) => { @@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w }); if (!areReconstructable) { - return { - disabled: true, - className: '!text-common-bright ohif-disabled', - disabledText: disabledText ?? 'Not available on the current viewport', - }; + return getDisabledState(disabledText); } const isMpr = protocol?.id === 'mpr'; @@ -288,11 +298,7 @@ function _evaluateToggle({ const toolName = toolbarService.getToolNameForButton(button); if (!toolGroup.hasTool(toolName)) { - return { - disabled: true, - className: '!text-common-bright ohif-disabled', - disabledText: disabledText ?? 'Not available on the current viewport', - }; + return getDisabledState(disabledText); } const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode); diff --git a/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts b/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts new file mode 100644 index 000000000..0f1c26a40 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts @@ -0,0 +1,219 @@ +import { useState, useEffect } from 'react'; +import debounce from 'lodash.debounce'; +import { roundNumber } from '@ohif/core/src/utils'; +import { + SegmentationData, + SegmentationRepresentation, +} from '../services/SegmentationService/SegmentationService'; + +const excludedModalities = ['SM', 'OT', 'DOC', 'ECG']; + +function mapSegmentationToDisplay(segmentation, customizationService) { + const { label, segments } = segmentation; + + // Get the readable text mapping once + const { readableText: readableTextMap } = customizationService.getCustomization( + 'PanelSegmentation.readableText', + {} + ); + + // Helper function to recursively map cachedStats to readable display text + function mapStatsToDisplay(stats, indent = 0) { + const primary = []; + const indentation = ' '.repeat(indent); + + for (const key in stats) { + if (Object.prototype.hasOwnProperty.call(stats, key)) { + const value = stats[key]; + const readableText = readableTextMap?.[key]; + + if (!readableText) { + continue; + } + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // Add empty row before category (except for the first category) + if (primary.length > 0) { + primary.push(''); + } + // Add category title + primary.push(`${indentation}${readableText}`); + // Recursively handle nested objects + primary.push(...mapStatsToDisplay(value, indent + 1)); + } else { + // For non-nested values, don't add empty rows + primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`); + } + } + } + + return primary; + } + + // Get customization for display text mapping + const displayTextMapper = segment => { + const defaultDisplay = { + primary: [], + secondary: [], + }; + + // If the segment has cachedStats, map it to readable text + if (segment.cachedStats) { + const primary = mapStatsToDisplay(segment.cachedStats); + defaultDisplay.primary = primary; + } + + return defaultDisplay; + }; + + const updatedSegments = {}; + + Object.entries(segments).forEach(([segmentIndex, segment]) => { + updatedSegments[segmentIndex] = { + ...segment, + displayText: displayTextMapper(segment), + }; + }); + + // Map the segments and apply the display text mapper + return { + ...segmentation, + label, + segments: updatedSegments, + }; +} + +/** + * Represents the combination of segmentation data and its representation in a viewport. + */ +type ViewportSegmentationRepresentation = { + segmentationsWithRepresentations: { + representation: SegmentationRepresentation; + segmentation: SegmentationData; + }[]; + disabled: boolean; +}; + +/** + * Custom hook that provides segmentation data and their representations for the active viewport. + * @param options - The options object. + * @param options.servicesManager - The services manager object. + * @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications. + * @param options.debounceTime - Debounce time in milliseconds for updates. + * @returns An array of segmentation data and their representations for the active viewport. + */ +export function useActiveViewportSegmentationRepresentations({ + servicesManager, + subscribeToDataModified = false, + debounceTime = 0, +}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation { + const { segmentationService, viewportGridService, customizationService, displaySetService } = + servicesManager.services; + + const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] = + useState({ + segmentationsWithRepresentations: [], + disabled: false, + }); + + useEffect(() => { + const update = () => { + const viewportId = viewportGridService.getActiveViewportId(); + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + if (!displaySetUIDs?.length) { + return; + } + + const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]); + + if (!displaySet) { + return; + } + + if (excludedModalities.includes(displaySet.Modality)) { + setSegmentationsWithRepresentations(prev => ({ + segmentationsWithRepresentations: [], + disabled: true, + })); + return; + } + + const segmentations = segmentationService.getSegmentations(); + + if (!segmentations?.length) { + setSegmentationsWithRepresentations(prev => ({ + segmentationsWithRepresentations: [], + disabled: false, + })); + return; + } + + const representations = segmentationService.getSegmentationRepresentations(viewportId); + + const newSegmentationsWithRepresentations = representations.map(representation => { + const segmentation = segmentationService.getSegmentation(representation.segmentationId); + const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService); + return { + representation, + segmentation: mappedSegmentation, + }; + }); + + setSegmentationsWithRepresentations({ + segmentationsWithRepresentations: newSegmentationsWithRepresentations, + disabled: false, + }); + }; + + const debouncedUpdate = + debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update; + + update(); + + const subscriptions = [ + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + debouncedUpdate + ), + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REMOVED, + debouncedUpdate + ), + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, + debouncedUpdate + ), + viewportGridService.subscribe( + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + debouncedUpdate + ), + viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate), + ]; + + if (subscribeToDataModified) { + subscriptions.push( + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + debouncedUpdate + ) + ); + } + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + if (debounceTime > 0) { + debouncedUpdate.cancel(); + } + }; + }, [ + segmentationService, + viewportGridService, + customizationService, + displaySetService, + debounceTime, + subscribeToDataModified, + ]); + + return segmentationsWithRepresentations; +} diff --git a/extensions/cornerstone/src/hooks/useMeasurements.ts b/extensions/cornerstone/src/hooks/useMeasurements.ts new file mode 100644 index 000000000..ba659a35a --- /dev/null +++ b/extensions/cornerstone/src/hooks/useMeasurements.ts @@ -0,0 +1,103 @@ +import { useState, useEffect } from 'react'; +import debounce from 'lodash.debounce'; + +function mapMeasurementToDisplay(measurement, displaySetService) { + const { referenceSeriesUID } = measurement; + + const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID); + + if (!displaySets[0]?.instances) { + throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.'); + } + + const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement; + + const firstSite = findingSites?.[0]; + const label = baseLabel || finding?.text || firstSite?.text || '(empty)'; + + // Initialize displayText with the structure used in Length.ts and CobbAngle.ts + const displayText = { + primary: [], + secondary: baseDisplayText?.secondary || [], + }; + + // Add baseDisplayText to primary if it exists + if (baseDisplayText) { + displayText.primary.push(...baseDisplayText.primary); + } + + // Add finding sites to primary + if (findingSites) { + findingSites.forEach(site => { + if (site?.text && site.text !== label) { + displayText.primary.push(site.text); + } + }); + } + + // Add finding to primary if it's different from the label + if (finding && finding.text && finding.text !== label) { + displayText.primary.push(finding.text); + } + + return { + ...measurement, + displayText, + label, + }; +} + +/** + * A custom hook that provides mapped measurements based on the given services and filters. + * + * @param {Object} servicesManager - The services manager object. + * @param {Object} options - The options for filtering and mapping measurements. + * @param {Function} options.measurementFilter - Optional function to filter measurements. + * @param {Object} options.valueTypes - The value types for mapping measurements. + * @returns {Array} An array of mapped and filtered measurements. + */ +export function useMeasurements(servicesManager, { measurementFilter }) { + const { measurementService, displaySetService } = servicesManager.services; + const [displayMeasurements, setDisplayMeasurements] = useState([]); + + useEffect(() => { + const updateDisplayMeasurements = () => { + let measurements = measurementService.getMeasurements(); + if (measurementFilter) { + measurements = measurements.filter(measurementFilter); + } + const mappedMeasurements = measurements.map(m => + mapMeasurementToDisplay(m, displaySetService) + ); + setDisplayMeasurements(prevMeasurements => { + if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) { + return mappedMeasurements; + } + return prevMeasurements; + }); + }; + + const debouncedUpdate = debounce(updateDisplayMeasurements, 100); + + updateDisplayMeasurements(); + + const events = [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + measurementService.EVENTS.MEASUREMENT_UPDATED, + measurementService.EVENTS.MEASUREMENT_REMOVED, + measurementService.EVENTS.MEASUREMENTS_CLEARED, + ]; + + const subscriptions = events.map( + evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe + ); + + return () => { + subscriptions.forEach(unsub => unsub()); + debouncedUpdate.cancel(); + }; + }, [measurementService, measurementFilter, displaySetService]); + + return displayMeasurements; +} diff --git a/extensions/cornerstone/src/hooks/useSegmentations.ts b/extensions/cornerstone/src/hooks/useSegmentations.ts new file mode 100644 index 000000000..835444c81 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useSegmentations.ts @@ -0,0 +1,148 @@ +import { useState, useEffect } from 'react'; +import debounce from 'lodash.debounce'; +import { roundNumber } from '@ohif/core/src/utils'; +import { SegmentationData } from '../services/SegmentationService/SegmentationService'; + +function mapSegmentationToDisplay(segmentation, customizationService) { + const { label, segments } = segmentation; + + // Get the readable text mapping once + const { readableText: readableTextMap } = customizationService.getCustomization( + 'PanelSegmentation.readableText', + {} + ); + + // Helper function to recursively map cachedStats to readable display text + function mapStatsToDisplay(stats, indent = 0) { + const primary = []; + const indentation = ' '.repeat(indent); + + for (const key in stats) { + if (Object.prototype.hasOwnProperty.call(stats, key)) { + const value = stats[key]; + const readableText = readableTextMap?.[key]; + + if (!readableText) { + continue; + } + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // Add empty row before category (except for the first category) + if (primary.length > 0) { + primary.push(''); + } + // Add category title + primary.push(`${indentation}${readableText}`); + // Recursively handle nested objects + primary.push(...mapStatsToDisplay(value, indent + 1)); + } else { + // For non-nested values, don't add empty rows + primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`); + } + } + } + + return primary; + } + + // Get customization for display text mapping + const displayTextMapper = segment => { + const defaultDisplay = { + primary: [], + secondary: [], + }; + + // If the segment has cachedStats, map it to readable text + if (segment.cachedStats) { + const primary = mapStatsToDisplay(segment.cachedStats); + defaultDisplay.primary = primary; + } + + return defaultDisplay; + }; + + const updatedSegments = {}; + + Object.entries(segments).forEach(([segmentIndex, segment]) => { + updatedSegments[segmentIndex] = { + ...segment, + displayText: displayTextMapper(segment), + }; + }); + + // Map the segments and apply the display text mapper + return { + ...segmentation, + label, + segments: updatedSegments, + }; +} + +/** + * Custom hook that provides segmentation data. + * @param options - The options object. + * @param options.servicesManager - The services manager object. + * @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications. + * @param options.debounceTime - Debounce time in milliseconds for updates. + * @returns An array of segmentation data. + */ +export function useSegmentations({ + servicesManager, + subscribeToDataModified = false, + debounceTime = 0, +}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] { + const { segmentationService, customizationService } = servicesManager.services; + + const [segmentations, setSegmentations] = useState([]); + + useEffect(() => { + const update = () => { + const segmentations = segmentationService.getSegmentations(); + + if (!segmentations?.length) { + setSegmentations([]); + return; + } + + const mappedSegmentations = segmentations.map(segmentation => + mapSegmentationToDisplay(segmentation, customizationService) + ); + + setSegmentations(mappedSegmentations); + }; + + const debouncedUpdate = + debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update; + + update(); + + const subscriptions = [ + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + debouncedUpdate + ), + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REMOVED, + debouncedUpdate + ), + ]; + + if (subscribeToDataModified) { + subscriptions.push( + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + debouncedUpdate + ) + ); + } + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + if (debounceTime > 0) { + debouncedUpdate.cancel(); + } + }; + }, [segmentationService, customizationService, debounceTime, subscribeToDataModified]); + + return segmentations; +} diff --git a/extensions/cornerstone/src/hps/mpr.ts b/extensions/cornerstone/src/hps/mpr.ts index fa54ffde6..99a12e038 100644 --- a/extensions/cornerstone/src/hps/mpr.ts +++ b/extensions/cornerstone/src/hps/mpr.ts @@ -1,5 +1,25 @@ import { Types } from '@ohif/core'; +const VOI_SYNC_GROUP = { + type: 'voi', + id: 'mpr', + source: true, + target: true, + options: { + syncColormap: true, + }, +}; + +const HYDRATE_SEG_SYNC_GROUP = { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, +}; + export const mpr: Types.HangingProtocol.Protocol = { id: 'mpr', name: 'MPR', @@ -10,7 +30,6 @@ export const mpr: Types.HangingProtocol.Protocol = { modifiedDate: '2023-08-15', availableTo: {}, editableBy: {}, - // Unknown number of priors referenced - so just match any study numberOfPriorsReferenced: 0, protocolMatchingRules: [], imageLoadStrategy: 'nth', @@ -71,17 +90,7 @@ export const mpr: Types.HangingProtocol.Protocol = { initialImageOptions: { preset: 'middle', }, - syncGroups: [ - { - type: 'voi', - id: 'mpr', - source: true, - target: true, - options: { - syncColormap: true, - }, - }, - ], + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], }, displaySets: [ { @@ -98,17 +107,7 @@ export const mpr: Types.HangingProtocol.Protocol = { initialImageOptions: { preset: 'middle', }, - syncGroups: [ - { - type: 'voi', - id: 'mpr', - source: true, - target: true, - options: { - syncColormap: true, - }, - }, - ], + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], }, displaySets: [ { @@ -125,17 +124,7 @@ export const mpr: Types.HangingProtocol.Protocol = { initialImageOptions: { preset: 'middle', }, - syncGroups: [ - { - type: 'voi', - id: 'mpr', - source: true, - target: true, - options: { - syncColormap: true, - }, - }, - ], + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], }, displaySets: [ { diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index bb6dc8bed..47d630c10 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -6,7 +6,6 @@ import { imageLoadPoolManager, imageRetrievalPoolManager, } from '@cornerstonejs/core'; -import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader'; import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import { Types } from '@ohif/core'; import Enums from './enums'; @@ -33,17 +32,28 @@ import { id } from './id'; import { measurementMappingUtils } from './utils/measurementServiceMappings'; import type { PublicViewportOptions } from './services/ViewportService/Viewport'; import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; -import { showLabelAnnotationPopup } from './utils/callInputDialog'; import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService'; import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider'; -import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel'; import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; import { findNearbyToolData } from './utils/findNearbyToolData'; import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer'; import { getSopClassHandlerModule } from './getSopClassHandlerModule'; +import { getDynamicVolumeInfo } from '@cornerstonejs/core/utilities'; +import { + useLutPresentationStore, + usePositionPresentationStore, + useSegmentationPresentationStore, + useSynchronizersStore, +} from './stores'; +import { useToggleOneUpViewportGridStore } from '../../default/src/stores/useToggleOneUpViewportGridStore'; +import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations'; +import { useMeasurements } from './hooks/useMeasurements'; +import getPanelModule from './getPanelModule'; +import PanelSegmentation from './panels/PanelSegmentation'; +import PanelMeasurement from './panels/PanelMeasurement'; +import DicomUpload from './components/DicomUpload/DicomUpload'; +import { useSegmentations } from './hooks/useSegmentations'; -const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader; -const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {}; const { imageRetrieveMetadataProvider } = cornerstone.utilities; const Component = React.lazy(() => { @@ -84,9 +94,8 @@ const cornerstoneExtension: Types.Extensions.Extension = { ]); toolbarService.registerEventForToolbarUpdate(segmentationService, [ - segmentationService.EVENTS.SEGMENTATION_ADDED, segmentationService.EVENTS.SEGMENTATION_REMOVED, - segmentationService.EVENTS.SEGMENTATION_UPDATED, + segmentationService.EVENTS.SEGMENTATION_MODIFIED, ]); toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [ @@ -108,9 +117,9 @@ const cornerstoneExtension: Types.Extensions.Extension = { // how to define them. imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions); }, - + getPanelModule, onModeExit: ({ servicesManager }: withAppTypes): void => { - const { cineService } = servicesManager.services; + const { cineService, segmentationService } = servicesManager.services; // Empty out the image load and retrieval pools to prevent memory leaks // on the mode exits Object.values(cs3DEnums.RequestType).forEach(type => { @@ -121,6 +130,13 @@ const cornerstoneExtension: Types.Extensions.Extension = { cineService.setIsCineEnabled(false); enabledElementReset(); + + useLutPresentationStore.getState().clearLutPresentationStore(); + usePositionPresentationStore.getState().clearPositionPresentationStore(); + useSynchronizersStore.getState().clearSynchronizersStore(); + useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore(); + useSegmentationPresentationStore.getState().clearSegmentationPresentationStore(); + segmentationService.removeAllSegmentations(); }, /** @@ -145,20 +161,12 @@ const cornerstoneExtension: Types.Extensions.Extension = { const { syncGroupService } = servicesManager.services; syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer); + servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', { + component: props => , + }); return init.call(this, props); }, - getToolbarModule, - getPanelModule({ servicesManager }) { - return [ - { - name: 'activeViewportWindowLevel', - component: () => { - return ; - }, - }, - ]; - }, getHangingProtocolModule, getViewportModule({ servicesManager, commandsManager }) { const ExtendedOHIFCornerstoneViewport = props => { @@ -196,7 +204,6 @@ const cornerstoneExtension: Types.Extensions.Extension = { }, getEnabledElement, dicomLoaderService, - showLabelAnnotationPopup, }, }, { @@ -235,6 +242,17 @@ export { ImageOverlayViewerTool, getSOPInstanceAttributes, dicomLoaderService, + // Export all stores + useLutPresentationStore, + usePositionPresentationStore, + useSegmentationPresentationStore, + useSynchronizersStore, Enums, + useMeasurements, + useActiveViewportSegmentationRepresentations, + useSegmentations, + PanelSegmentation, + PanelMeasurement, + DicomUpload, }; export default cornerstoneExtension; diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index df0dc07c5..4b9c13d54 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -1,4 +1,4 @@ -import OHIF, { Types, errorHandler } from '@ohif/core'; +import OHIF, { errorHandler } from '@ohif/core'; import React from 'react'; import * as cornerstone from '@cornerstonejs/core'; @@ -13,12 +13,13 @@ import { getEnabledElement, Settings, utilities as csUtilities, - Enums as csEnums, } from '@cornerstonejs/core'; import { cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, -} from '@cornerstonejs/streaming-image-volume-loader'; +} from '@cornerstonejs/core/loaders'; + +import RequestTypes from '@cornerstonejs/core/enums/RequestType'; import initWADOImageLoader from './initWADOImageLoader'; import initCornerstoneTools from './initCornerstoneTools'; @@ -33,6 +34,10 @@ import initContextMenu from './initContextMenu'; import initDoubleClick from './initDoubleClick'; import initViewTiming from './utils/initViewTiming'; import { colormaps } from './utils/colormaps'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { useLutPresentationStore } from './stores/useLutPresentationStore'; +import { usePositionPresentationStore } from './stores/usePositionPresentationStore'; +import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore'; const { registerColormap } = csUtilities.colormap; @@ -46,27 +51,12 @@ export default async function init({ servicesManager, commandsManager, extensionManager, - appConfig -}: Types.Extensions.ExtensionParams): Promise { + appConfig, +}: withAppTypes): Promise { // Note: this should run first before initializing the cornerstone // DO NOT CHANGE THE ORDER - const value = appConfig.useSharedArrayBuffer; - let sharedArrayBufferDisabled = false; - - if (value === 'AUTO') { - cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.AUTO); - } else if (value === 'FALSE' || value === false) { - cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.FALSE); - sharedArrayBufferDisabled = true; - } else { - cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.TRUE); - } await cs3DInit({ - rendering: { - preferSizeOverAccuracy: Boolean(appConfig.preferSizeOverAccuracy), - useNorm16Texture: Boolean(appConfig.useNorm16Texture), - }, peerImport: appConfig.peerImport, }); @@ -99,63 +89,39 @@ export default async function init({ cornerstoneViewportService, hangingProtocolService, viewportGridService, - stateSyncService, - studyPrefetcherService, } = servicesManager.services; window.services = servicesManager.services; window.extensionManager = extensionManager; window.commandsManager = commandsManager; - if ( - appConfig.showWarningMessageForCrossOrigin && - !window.crossOriginIsolated && - !sharedArrayBufferDisabled - ) { - uiNotificationService.show({ - title: 'Cross Origin Isolation', - message: - 'Cross Origin Isolation is not enabled, read more about it here: https://docs.ohif.org/faq/', - type: 'warning', - }); - } - if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) { _showCPURenderingModal(uiModalService, hangingProtocolService); } + const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState(); - // Stores a map from `lutPresentationId` to a Presentation object so that - // an OHIFCornerstoneViewport can be redisplayed with the same LUT - stateSyncService.register('lutPresentationStore', { clearOnModeExit: true }); + const { getPresentationId: getSegmentationPresentationId } = + useSegmentationPresentationStore.getState(); - // Stores synchronizers state to be restored - stateSyncService.register('synchronizersStore', { clearOnModeExit: true }); + const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState(); - // Stores a map from `positionPresentationId` to a Presentation object so that - // an OHIFCornerstoneViewport can be redisplayed with the same position - stateSyncService.register('positionPresentationStore', { - clearOnModeExit: true, - }); + // register presentation id providers + viewportGridService.addPresentationIdProvider( + 'positionPresentationId', + getPositionPresentationId + ); + viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId); + viewportGridService.addPresentationIdProvider( + 'segmentationPresentationId', + getSegmentationPresentationId + ); - // Stores the entire ViewportGridService getState when toggling to one up - // (e.g. via a double click) so that it can be restored when toggling back. - stateSyncService.register('toggleOneUpViewportGridStore', { - clearOnModeExit: true, - }); - - const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap; - const contourRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Contour; - - cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, { - fillAlpha: 0.5, - fillAlphaInactive: 0.2, - outlineOpacity: 1, - outlineOpacityInactive: 0.65, - }); - - cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(contourRepresentation, { - renderFill: false, - }); + cornerstoneTools.segmentation.config.style.setStyle( + { type: SegmentationRepresentations.Contour }, + { + renderFill: false, + } + ); const metadataProvider = OHIF.classes.MetadataProvider; @@ -184,9 +150,10 @@ export default async function init({ // These are set reasonably low to allow for interleaved retrieves and slower // connections. imageLoadPoolManager.maxNumRequests = { - interaction: appConfig?.maxNumRequests?.interaction || 10, - thumbnail: appConfig?.maxNumRequests?.thumbnail || 5, - prefetch: appConfig?.maxNumRequests?.prefetch || 5, + [RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10, + [RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5, + [RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5, + [RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10, }; initWADOImageLoader(userAuthenticationService, appConfig, extensionManager); @@ -201,17 +168,23 @@ export default async function init({ hangingProtocolService.subscribe( hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, volumeInputArrayMap => { + const { lutPresentationStore } = useLutPresentationStore.getState(); + const { segmentationPresentationStore } = useSegmentationPresentationStore.getState(); + const { positionPresentationStore } = usePositionPresentationStore.getState(); + for (const entry of volumeInputArrayMap.entries()) { const [viewportId, volumeInputArray] = entry; const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId); - const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState(); const { presentationIds } = ohifViewport.getViewportOptions(); + const presentations = { positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId], lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId], + segmentationPresentation: + segmentationPresentationStore[presentationIds?.segmentationPresentationId], }; cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations); @@ -249,10 +222,6 @@ export default async function init({ handler(detail.error); }; - eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => { - const { element } = evt.detail; - cornerstoneTools.utilities.stackContextPrefetch.enable(element); - }); eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler); eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler); @@ -269,26 +238,11 @@ export default async function init({ commandsManager.runCommand('resetCrosshairs', { viewportId }); }); - // eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener); - initViewTiming({ element }); } - function elementDisabledHandler(evt) { - const { element } = evt.detail; - - // element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs); - - // TODO - consider removing the callback when all elements are gone - // eventTarget.removeEventListener( - // EVENTS.STACK_VIEWPORT_NEW_STACK, - // newStackCallback - // ); - } - eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null)); - eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null)); colormaps.forEach(registerColormap); // Event listener @@ -301,8 +255,45 @@ export default async function init({ type: 'error', }); }, - 1000 + 100 ); + + // Call this function when initializing + initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService); +} + +function initializeWebWorkerProgressHandler(uiNotificationService) { + const activeToasts = new Map(); + + eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => { + const { progress, type, id } = detail; + + const cacheKey = `${type}-${id}`; + if (progress === 0 && !activeToasts.has(cacheKey)) { + const progressPromise = new Promise((resolve, reject) => { + activeToasts.set(cacheKey, { resolve, reject }); + }); + + uiNotificationService.show({ + id: cacheKey, + title: `${type}`, + message: `${type}: ${progress}%`, + autoClose: false, + promise: progressPromise, + promiseMessages: { + loading: `Computing...`, + success: `Completed successfully`, + error: 'Web Worker failed', + }, + }); + } else { + if (progress === 100) { + const { resolve } = activeToasts.get(cacheKey); + resolve({ progress, type }); + activeToasts.delete(cacheKey); + } + } + }); } function CPUModal() { diff --git a/extensions/cornerstone/src/initCineService.ts b/extensions/cornerstone/src/initCineService.ts index b98c2f281..f5fbfb998 100644 --- a/extensions/cornerstone/src/initCineService.ts +++ b/extensions/cornerstone/src/initCineService.ts @@ -1,12 +1,9 @@ -import { cache } from '@cornerstonejs/core'; +import { cache, Types } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/tools'; -function _getVolumesFromViewport(viewport) { - return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : []; -} - -function _getVolumeFromViewport(viewport) { - const volumes = _getVolumesFromViewport(viewport).filter(volume => volume); +function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) { + const volumeIds = viewport.getAllVolumeIds(); + const volumes = volumeIds.map(id => cache.getVolume(id)); const dynamicVolume = volumes.find(volume => volume.isDynamicVolume()); return dynamicVolume ?? volumes[0]; diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js index bfabc4a01..dd1aedc0f 100644 --- a/extensions/cornerstone/src/initCornerstoneTools.js +++ b/extensions/cornerstone/src/initCornerstoneTools.js @@ -2,9 +2,8 @@ import { PanTool, WindowLevelTool, StackScrollTool, - StackScrollMouseWheelTool, + VolumeRotateTool, ZoomTool, - VolumeRotateMouseWheelTool, MIPJumpToClickTool, LengthTool, RectangleROITool, @@ -19,7 +18,6 @@ import { CobbAngleTool, MagnifyTool, CrosshairsTool, - SegmentationDisplayTool, RectangleScissorsTool, SphereScissorsTool, CircleScissorsTool, @@ -47,15 +45,15 @@ export default function initCornerstoneTools(configuration = {}) { CrosshairsTool.isAnnotation = false; ReferenceLinesTool.isAnnotation = false; AdvancedMagnifyTool.isAnnotation = false; + PlanarFreehandContourSegmentationTool.isAnnotation = false; init(configuration); addTool(PanTool); addTool(WindowLevelTool); - addTool(StackScrollMouseWheelTool); addTool(StackScrollTool); + addTool(VolumeRotateTool); addTool(ZoomTool); addTool(ProbeTool); - addTool(VolumeRotateMouseWheelTool); addTool(MIPJumpToClickTool); addTool(LengthTool); addTool(RectangleROITool); @@ -69,7 +67,6 @@ export default function initCornerstoneTools(configuration = {}) { addTool(CobbAngleTool); addTool(MagnifyTool); addTool(CrosshairsTool); - addTool(SegmentationDisplayTool); addTool(RectangleScissorsTool); addTool(SphereScissorsTool); addTool(CircleScissorsTool); @@ -108,9 +105,8 @@ const toolNames = { ArrowAnnotate: ArrowAnnotateTool.toolName, WindowLevel: WindowLevelTool.toolName, StackScroll: StackScrollTool.toolName, - StackScrollMouseWheel: StackScrollMouseWheelTool.toolName, Zoom: ZoomTool.toolName, - VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName, + VolumeRotate: VolumeRotateTool.toolName, MipJumpToClick: MIPJumpToClickTool.toolName, Length: LengthTool.toolName, DragProbe: DragProbeTool.toolName, @@ -124,7 +120,6 @@ const toolNames = { CobbAngle: CobbAngleTool.toolName, Magnify: MagnifyTool.toolName, Crosshairs: CrosshairsTool.toolName, - SegmentationDisplay: SegmentationDisplayTool.toolName, Brush: BrushTool.toolName, PaintFill: PaintFillTool.toolName, ReferenceLines: ReferenceLinesTool.toolName, diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index 19ccac95d..1672ea79f 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -1,4 +1,4 @@ -import { eventTarget } from '@cornerstonejs/core'; +import { eventTarget, Types } from '@cornerstonejs/core'; import { Enums, annotation } from '@cornerstonejs/tools'; import { DicomMetadataStore } from '@ohif/core'; @@ -7,6 +7,7 @@ import { toolNames } from './initCornerstoneTools'; import { onCompletedCalibrationLine } from './tools/CalibrationLineTool'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; +import { triggerAnnotationRenderForViewportIds } from '@cornerstonejs/tools/utilities'; const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; const { removeAnnotation } = annotation.state; @@ -344,8 +345,7 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor return; } - const { uid, label } = measurement; - + const { uid, label, isLocked, isVisible } = measurement; const sourceAnnotation = annotation.state.getAnnotation(uid); const { data, metadata } = sourceAnnotation; @@ -361,7 +361,23 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor data.text = label; } - // Todo: trigger render for annotation + // update the isLocked state + annotation.locking.setAnnotationLocked(uid, isLocked); + + // update the isVisible state + annotation.visibility.setAnnotationVisibility(uid, isVisible); + + // annotation.config.style.setAnnotationStyles(uid, { + // color: `rgb(${color[0]}, ${color[1]}, ${color[2]})`, + // }); + + // I don't like this but will fix later + const renderingEngine = + cornerstoneViewportService.getRenderingEngine() as Types.IRenderingEngine; + // Note: We could do a better job by triggering the render on the + // viewport itself, but the removeAnnotation does not include that info... + const viewportIds = renderingEngine.getViewports().map(viewport => viewport.id); + triggerAnnotationRenderForViewportIds(viewportIds); } ); diff --git a/extensions/cornerstone/src/initWADOImageLoader.js b/extensions/cornerstone/src/initWADOImageLoader.js index 5a200849f..9f9922f7d 100644 --- a/extensions/cornerstone/src/initWADOImageLoader.js +++ b/extensions/cornerstone/src/initWADOImageLoader.js @@ -1,47 +1,18 @@ -import * as cornerstone from '@cornerstonejs/core'; import { volumeLoader } from '@cornerstonejs/core'; import { cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, -} from '@cornerstonejs/streaming-image-volume-loader'; -import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader'; -import dicomParser from 'dicom-parser'; +} from '@cornerstonejs/core/loaders'; +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; import { errorHandler, utils } from '@ohif/core'; const { registerVolumeLoader } = volumeLoader; -let initialized = false; - -function initWebWorkers(appConfig) { - const config = { - maxWebWorkers: Math.min( - Math.max(navigator.hardwareConcurrency - 1, 1), - appConfig.maxNumberOfWebWorkers - ), - startWebWorkersOnDemand: true, - taskConfiguration: { - decodeTask: { - initializeCodecsOnStartup: false, - usePDFJS: false, - strict: false, - }, - }, - }; - - if (!initialized) { - dicomImageLoader.webWorkerManager.initialize(config); - initialized = true; - } -} - export default function initWADOImageLoader( userAuthenticationService, appConfig, extensionManager ) { - dicomImageLoader.external.cornerstone = cornerstone; - dicomImageLoader.external.dicomParser = dicomParser; - registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader); registerVolumeLoader( @@ -49,17 +20,11 @@ export default function initWADOImageLoader( cornerstoneStreamingDynamicImageVolumeLoader ); - dicomImageLoader.configure({ - decodeConfig: { - // !! IMPORTANT !! - // We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader - // will convert everything to integers (to be able to work with cornerstone-2d). - // Until the default is set to true (which is the case for cornerstone3D), - // we should set this flag to false. - convertFloatPixelDataToInt: false, - use16BitDataType: - Boolean(appConfig.useNorm16Texture) || Boolean(appConfig.preferSizeOverAccuracy), - }, + dicomImageLoader.init({ + maxWebWorkers: Math.min( + Math.max(navigator.hardwareConcurrency - 1, 1), + appConfig.maxNumberOfWebWorkers + ), beforeSend: function (xhr) { //TODO should be removed in the future and request emitted by DicomWebDataSource const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {}; @@ -84,16 +49,8 @@ export default function initWADOImageLoader( errorHandler.getHTTPErrorHandler(error); }, }); - - initWebWorkers(appConfig); } export function destroy() { - // Note: we don't want to call .terminate on the webWorkerManager since - // that resets the config - const webWorkers = webWorkerManager.webWorkers; - for (let i = 0; i < webWorkers.length; i++) { - webWorkers[i].worker.terminate(); - } - webWorkers.length = 0; + console.debug('Destroying WADO Image Loader'); } diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx new file mode 100644 index 000000000..3652b31bb --- /dev/null +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useRef } from 'react'; +import { useViewportGrid } from '@ohif/ui'; +import { MeasurementTable } from '@ohif/ui-next'; +import debounce from 'lodash.debounce'; +import { useMeasurements } from '../hooks/useMeasurements'; +import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default'; + +export default function PanelMeasurementTable({ + servicesManager, + customHeader, + measurementFilter, +}: withAppTypes): React.ReactNode { + const measurementsPanelRef = useRef(null); + + const [viewportGrid] = useViewportGrid(); + const { measurementService, customizationService, uiDialogService } = servicesManager.services; + + const displayMeasurements = useMeasurements(servicesManager, { + measurementFilter, + }); + + useEffect(() => { + if (displayMeasurements.length > 0) { + debounce(() => { + measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight; + }, 300)(); + } + }, [displayMeasurements.length]); + + const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => { + if (isActive) { + return; + } + + const measurements = [...displayMeasurements]; + const measurement = measurements.find(m => m.uid === uid); + + measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true)); + measurement.isActive = true; + }; + + const jumpToImage = (uid: string) => { + measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); + onMeasurementItemClickHandler(uid, true); + }; + + const removeMeasurement = (uid: string) => { + measurementService.remove(uid); + }; + + const renameMeasurement = (uid: string) => { + jumpToImage(uid); + const labelConfig = customizationService.get('measurementLabels'); + const measurement = measurementService.getMeasurement(uid); + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => { + measurementService.update( + uid, + { + ...val, + }, + true + ); + }); + }; + + const changeColorMeasurement = (uid: string) => { + const { color } = measurementService.getMeasurement(uid); + const rgbaColor = { + r: color[0], + g: color[1], + b: color[2], + a: color[3] / 255.0, + }; + colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { + if (actionId === 'cancel') { + return; + } + + const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; + // segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); + }); + }; + + const toggleLockMeasurement = (uid: string) => { + measurementService.toggleLockMeasurement(uid); + }; + + const toggleVisibilityMeasurement = (uid: string) => { + measurementService.toggleVisibilityMeasurement(uid); + }; + + const measurements = displayMeasurements.filter( + dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId + ); + const additionalFindings = displayMeasurements.filter( + dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId + ); + + return ( + <> +
+ + + {customHeader && ( + <> + {typeof customHeader === 'function' + ? customHeader({ + additionalFindings, + measurements, + }) + : customHeader} + + )} + + + + {additionalFindings.length > 0 && ( + + + + )} +
+ + ); +} diff --git a/extensions/cornerstone/src/panels/PanelSegmentation.tsx b/extensions/cornerstone/src/panels/PanelSegmentation.tsx new file mode 100644 index 000000000..b9e9ffcd2 --- /dev/null +++ b/extensions/cornerstone/src/panels/PanelSegmentation.tsx @@ -0,0 +1,230 @@ +import React from 'react'; +import { SegmentationTable } from '@ohif/ui-next'; +import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations'; +import { metaData } from '@cornerstonejs/core'; + +export default function PanelSegmentation({ + servicesManager, + commandsManager, + extensionManager, + configuration, + children, +}: withAppTypes) { + const { customizationService, viewportGridService, displaySetService } = servicesManager.services; + + const { segmentationsWithRepresentations, disabled } = + useActiveViewportSegmentationRepresentations({ + servicesManager, + }); + + const handlers = { + onSegmentationAdd: async () => { + const viewportId = viewportGridService.getState().activeViewportId; + commandsManager.run('createLabelmapForViewport', { viewportId }); + }, + + onSegmentationClick: (segmentationId: string) => { + commandsManager.run('setActiveSegmentation', { segmentationId }); + }, + + onSegmentAdd: segmentationId => { + commandsManager.run('addSegment', { segmentationId }); + }, + + onSegmentClick: (segmentationId, segmentIndex) => { + commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex }); + }, + + onSegmentEdit: (segmentationId, segmentIndex) => { + commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex }); + }, + + onSegmentationEdit: segmentationId => { + commandsManager.run('editSegmentationLabel', { segmentationId }); + }, + + onSegmentColorClick: (segmentationId, segmentIndex) => { + commandsManager.run('editSegmentColor', { segmentationId, segmentIndex }); + }, + + onSegmentDelete: (segmentationId, segmentIndex) => { + commandsManager.run('deleteSegment', { segmentationId, segmentIndex }); + }, + + onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => { + commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type }); + }, + + onToggleSegmentLock: (segmentationId, segmentIndex) => { + commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex }); + }, + + onToggleSegmentationRepresentationVisibility: (segmentationId, type) => { + commandsManager.run('toggleSegmentationVisibility', { segmentationId, type }); + }, + + onSegmentationDownload: segmentationId => { + commandsManager.run('downloadSegmentation', { segmentationId }); + }, + + storeSegmentation: async segmentationId => { + commandsManager.run('storeSegmentation', { segmentationId }); + }, + + onSegmentationDownloadRTSS: segmentationId => { + commandsManager.run('downloadRTSS', { segmentationId }); + }, + + setStyle: (segmentationId, type, key, value) => { + commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value }); + }, + + toggleRenderInactiveSegmentations: () => { + commandsManager.run('toggleRenderInactiveSegmentations'); + }, + + onSegmentationRemoveFromViewport: segmentationId => { + commandsManager.run('removeSegmentationFromViewport', { segmentationId }); + }, + + onSegmentationDelete: segmentationId => { + commandsManager.run('deleteSegmentation', { segmentationId }); + }, + + setFillAlpha: (type, value) => { + commandsManager.run('setFillAlpha', { type, value }); + }, + + setOutlineWidth: (type, value) => { + commandsManager.run('setOutlineWidth', { type, value }); + }, + + setRenderFill: (type, value) => { + commandsManager.run('setRenderFill', { type, value }); + }, + + setRenderOutline: (type, value) => { + commandsManager.run('setRenderOutline', { type, value }); + }, + + setFillAlphaInactive: (type, value) => { + commandsManager.run('setFillAlphaInactive', { type, value }); + }, + + getRenderInactiveSegmentations: () => { + return commandsManager.run('getRenderInactiveSegmentations'); + }, + }; + + const { mode: SegmentationTableMode } = customizationService.getCustomization( + 'PanelSegmentation.tableMode', + { + id: 'default.segmentationTable.mode', + mode: 'collapsed', + } + ); + + // custom onSegmentationAdd if provided + const { onSegmentationAdd } = customizationService.getCustomization( + 'PanelSegmentation.onSegmentationAdd', + { + id: 'segmentation.onSegmentationAdd', + onSegmentationAdd: handlers.onSegmentationAdd, + } + ); + + const { disableEditing } = customizationService.getCustomization( + 'PanelSegmentation.disableEditing', + { + id: 'default.disableEditing', + disableEditing: false, + } + ); + + const { showAddSegment } = customizationService.getCustomization( + 'PanelSegmentation.showAddSegment', + { + id: 'default.showAddSegment', + showAddSegment: true, + } + ); + + const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => { + const { representationData, segmentationId } = segmentation; + const { Labelmap } = representationData; + const referencedImageIds = Labelmap.referencedImageIds; + const firstImageId = referencedImageIds[0]; + + const instance = metaData.get('instance', firstImageId); + const { SOPInstanceUID, SeriesInstanceUID } = instance; + + const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + const isExportable = displaySet.isReconstructable; + + return { + segmentationId, + isExportable, + }; + }); + + return ( + <> + + {children} + + + + {SegmentationTableMode === 'collapsed' ? ( + + + + + + ) : ( + + + {/* */} + + + )} + + + ); +} diff --git a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts index a8a4620ac..9c62ff492 100644 --- a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts +++ b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts @@ -48,6 +48,17 @@ export default class ColorbarService extends PubSubService { super(ColorbarService.EVENTS); } + /** + * Gets the volume ID for a given identifier by searching through the viewport's volume IDs. + * @param viewport - The viewport instance to search volumes in + * @param searchId - The identifier to search for within volume IDs + * @returns The matching volume ID if found, null otherwise + */ + private getVolumeIdForIdentifier(viewport, searchId: string): string | null { + const volumeIds = viewport.getAllVolumeIds?.() || []; + return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null; + } + /** * Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`. * This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options. @@ -59,8 +70,18 @@ export default class ColorbarService extends PubSubService { public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) { const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); const viewport = renderingEngine.getViewport(viewportId); + + if (!viewport) { + return; + } + const { element } = viewport; const actorEntries = viewport.getActors(); + + if (!actorEntries || actorEntries.length === 0) { + return; + } + const { position, width: thickness, activeColormapName, colormaps } = options; const numContainers = displaySetInstanceUIDs.length; @@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService { ); displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => { - const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID)); - const volumeId = actorEntry?.uid; + const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID); const properties = viewport?.getProperties(volumeId); const colormap = properties?.colormap; - // if there's an initial colormap set, and no colormap on the viewport, set it if (activeColormapName && !colormap) { this.setViewportColormap( viewportId, @@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService { return; } const setViewportProperties = (viewport, uid) => { - const actorEntry = actorEntries.find(entry => entry.uid.includes(uid)); - const { actor: volumeActor, uid: volumeId } = actorEntry; - viewport.setProperties({ colormap, volumeActor }, volumeId); + const volumeId = this.getVolumeIdForIdentifier(viewport, uid); + viewport.setProperties({ colormap }, volumeId); }; if (viewport instanceof StackViewport) { diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index 80454ac98..b185f6cdd 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -1,5 +1,5 @@ import { Types } from '@ohif/core'; -import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core'; +import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; @@ -33,27 +33,11 @@ class CornerstoneCacheService { public async createViewportData( displaySets: Types.DisplaySet[], - viewportOptions: Record, + viewportOptions: AppTypes.ViewportGrid.GridViewportOptions, dataSource: unknown, initialImageIndex?: number ): Promise { - let viewportType = viewportOptions.viewportType as string; - - // Todo: Since Cornerstone 3D currently doesn't support segmentation - // on stack viewport, we should check if whether the the displaySets - // that are about to be displayed are referenced in a segmentation - // as a reference volume, if so, we should hang a volume viewport - // instead of a stack viewport - if (this._shouldRenderSegmentation(displaySets)) { - // if the viewport type is volume 3D, we should let it be as it is - // Todo: in future here we should kick start the conversion of the - // segmentation to closed surface - viewportType = - viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume'; - - // update viewportOptions to reflect the new viewport type - viewportOptions.viewportType = viewportType; - } + const viewportType = viewportOptions.viewportType as string; const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets); let viewportData: StackViewportData | VolumeViewportData; @@ -86,7 +70,7 @@ class CornerstoneCacheService { } public async invalidateViewportData( - viewportData: VolumeViewportData, + viewportData: VolumeViewportData | StackViewportData, invalidatedDisplaySetInstanceUID: string, dataSource, displaySetService @@ -176,9 +160,6 @@ class CornerstoneCacheService { ): Promise { const { uiNotificationService } = this.servicesManager.services; const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet); - const nonOverlayDisplaySets = displaySets.filter(ds => !ds.isOverlayDisplaySet); - - // load overlays if they are not loaded for (const overlayDisplaySet of overlayDisplaySets) { if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) { const { userAuthenticationService } = this.servicesManager.services; @@ -187,7 +168,7 @@ class CornerstoneCacheService { await overlayDisplaySet.load({ headers }); } catch (e) { uiNotificationService.show({ - title: 'Error loading overlayDisplaySet', + title: 'Error loading displaySet', message: e.message, type: 'error', }); @@ -196,43 +177,43 @@ class CornerstoneCacheService { } } - const displaySet = nonOverlayDisplaySets[0]; - - if (displaySet.load && displaySet.load instanceof Function) { - const { userAuthenticationService } = this.servicesManager.services; - const headers = userAuthenticationService.getAuthorizationHeader(); - - try { - await displaySet.load({ headers }); - } catch (e) { - uiNotificationService.show({ - title: 'Error loading displaySet', - message: e.message, - type: 'error', - }); - console.error(e); - } - } - - let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID); - - if (!stackImageIds) { - stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); - this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds); - } - // Ensuring the first non-overlay `displaySet` is always the primary one - const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => { - const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds; + const StackViewportData = []; + for (const displaySet of displaySets) { + const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet; - return { + if (displaySet.load && displaySet.load instanceof Function) { + const { userAuthenticationService } = this.servicesManager.services; + const headers = userAuthenticationService.getAuthorizationHeader(); + try { + await displaySet.load({ headers }); + } catch (e) { + uiNotificationService.show({ + title: 'Error loading displaySet', + message: e.message, + type: 'error', + }); + console.error(e); + } + } + + let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID); + + if (!stackImageIds) { + stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); + // assign imageIds to the displaySet + displaySet.imageIds = stackImageIds; + this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds); + } + + StackViewportData.push({ StudyInstanceUID, displaySetInstanceUID, isCompositeStack, imageIds: stackImageIds, initialImageIndex, - }; - }); + }); + } return { viewportType, @@ -253,6 +234,7 @@ class CornerstoneCacheService { for (const displaySet of displaySets) { const { Modality } = displaySet; const isParametricMap = Modality === 'PMAP'; + const isSeg = Modality === 'SEG'; // Don't create volumes for the displaySets that have custom load // function (e.g., SEG, RT, since they rely on the reference volumes @@ -295,7 +277,7 @@ class CornerstoneCacheService { // Parametric maps do not have image ids but they already have volume data // therefore a new volume should not be created. - if (!isParametricMap && (!volumeImageIds || !volume)) { + if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) { volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource); volume = await volumeLoader.createAndCacheVolume(volumeId, { @@ -303,6 +285,9 @@ class CornerstoneCacheService { }); this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds); + + // Add imageIds to the displaySet for volumes + displaySet.imageIds = volumeImageIds; } volumeData.push({ @@ -321,39 +306,15 @@ class CornerstoneCacheService { }; } - private _shouldRenderSegmentation(displaySets) { - const { segmentationService, displaySetService } = this.servicesManager.services; - - const viewportDisplaySetInstanceUIDs = displaySets.map( - ({ displaySetInstanceUID }) => displaySetInstanceUID - ); - - // check inside segmentations if any of them are referencing the displaySets - // that are about to be displayed - const segmentations = segmentationService.getSegmentations(); - - for (const segmentation of segmentations) { - const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID; - const segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID); - - const instance = segDisplaySet.instances?.[0] || segDisplaySet.instance; - - const shouldDisplaySeg = segmentationService.shouldRenderSegmentation( - viewportDisplaySetInstanceUIDs, - instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID - ); - - if (shouldDisplaySeg) { - return true; - } - } - } - private _getCornerstoneStackImageIds(displaySet, dataSource): string[] { return dataSource.getImageIdsForDisplaySet(displaySet); } private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] { + if (displaySet.imageIds) { + return displaySet.imageIds; + } + const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); return stackImageIds; diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index af98398ff..40825c208 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -1,40 +1,75 @@ -import { Types as OhifTypes, PubSubService } from '@ohif/core'; import { cache, Enums as csEnums, - geometryLoader, eventTarget, - getEnabledElementByIds, + geometryLoader, + getEnabledElementByViewportId, + imageLoader, + Types as csTypes, utilities as csUtils, - volumeLoader, - StackViewport, } from '@cornerstonejs/core'; import { Enums as csToolsEnums, segmentation as cstSegmentation, Types as cstTypes, - utilities as cstUtils, } from '@cornerstonejs/tools'; -import isEqual from 'lodash.isequal'; -import { Types as ohifTypes } from '@ohif/core'; +import { PubSubService, Types as OHIFTypes } from '@ohif/core'; import { easeInOutBell, reverseEaseInOutBell } from '../../utils/transitions'; -import { Segment, Segmentation, SegmentationConfig } from './SegmentationServiceTypes'; import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { addColorLUT } from '@cornerstonejs/tools/segmentation/addColorLUT'; +import { getNextColorLUTIndex } from '@cornerstonejs/tools/segmentation/getNextColorLUTIndex'; +import { Segment } from '@cornerstonejs/tools/types/SegmentationStateTypes'; +import { ContourStyle, LabelmapStyle, SurfaceStyle } from '@cornerstonejs/tools/types'; +import { ViewportType } from '@cornerstonejs/core/enums'; +import { SegmentationPresentation, SegmentationPresentationItem } from '../../types/Presentation'; +import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/segmentation/updateLabelmapSegmentationImageReferences'; +import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents'; +import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap'; +import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation'; const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap; const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour; +export type SegmentRepresentation = { + segmentIndex: number; + color: csTypes.Color; + opacity: number; + visible: boolean; +}; + +export type SegmentationData = cstTypes.Segmentation; + +export type SegmentationRepresentation = cstTypes.SegmentationRepresentation & { + viewportId: string; + id: string; + label: string; + styles: cstTypes.RepresentationStyle; + segments: { + [key: number]: SegmentRepresentation; + }; +}; + +export type SegmentationInfo = { + segmentation: SegmentationData; + representation?: SegmentationRepresentation; +}; + const EVENTS = { - // fired when the segmentation is updated (e.g. when a segment is added, removed, or modified, locked, visibility changed etc.) - SEGMENTATION_UPDATED: 'event::segmentation_updated', - // fired when the segmentation data (e.g., labelmap pixels) is modified - SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified', - // fired when the segmentation is added to the cornerstone + SEGMENTATION_MODIFIED: 'event::segmentation_modified', + // fired when the segmentation is added SEGMENTATION_ADDED: 'event::segmentation_added', + // + SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified', // fired when the segmentation is removed SEGMENTATION_REMOVED: 'event::segmentation_removed', - // fired when the configuration for the segmentation is changed (e.g., brush size, render fill, outline thickness, etc.) - SEGMENTATION_CONFIGURATION_CHANGED: 'event::segmentation_configuration_changed', + // + // fired when segmentation representation is added + SEGMENTATION_REPRESENTATION_MODIFIED: 'event::segmentation_representation_modified', + // fired when segmentation representation is removed + SEGMENTATION_REPRESENTATION_REMOVED: 'event::segmentation_representation_removed', + // + // LOADING EVENTS // fired when the active segment is loaded in SEG or RTSTRUCT SEGMENT_LOADING_COMPLETE: 'event::segment_loading_complete', // loading completed for all segments @@ -43,37 +78,147 @@ const EVENTS = { const VALUE_TYPES = {}; -const SEGMENT_CONSTANT = { - opacity: 255, - isVisible: true, - isLocked: false, -}; - const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; class SegmentationService extends PubSubService { static REGISTRATION = { name: 'segmentationService', altName: 'SegmentationService', - create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams): SegmentationService => { + create: ({ servicesManager }: OHIFTypes.Extensions.ExtensionParams): SegmentationService => { return new SegmentationService({ servicesManager }); }, }; - segmentations: Record; + private _segmentationIdToColorLUTIndexMap: Map; readonly servicesManager: AppTypes.ServicesManager; highlightIntervalId = null; readonly EVENTS = EVENTS; constructor({ servicesManager }) { super(EVENTS); - this.segmentations = {}; + + this._segmentationIdToColorLUTIndexMap = new Map(); this.servicesManager = servicesManager; this._initSegmentationService(); } + /** + * Retrieves a segmentation by its ID. + * + * @param segmentationId - The unique identifier of the segmentation to retrieve. + * @returns The segmentation object if found, or undefined if not found. + * + * @remarks + * This method directly accesses the cornerstone tools segmentation state to fetch + * the segmentation data. It's useful when you need to access specific properties + * or perform operations on a particular segmentation. + */ + public getSegmentation(segmentationId: string): cstTypes.Segmentation | undefined { + return cstSegmentation.state.getSegmentation(segmentationId); + } + + /** + * Retrieves all segmentations from the cornerstone tools segmentation state. + * + * @returns An array of all segmentations currently stored in the state + * + * @remarks + * This is a convenience method that directly accesses the cornerstone tools + * segmentation state to get all available segmentations. It returns the raw + * segmentation objects without any additional processing or filtering. + */ + public getSegmentations(): cstTypes.Segmentation[] | [] { + return cstSegmentation.state.getSegmentations(); + } + + public getPresentation(viewportId: string): SegmentationPresentation { + const segmentationPresentations: SegmentationPresentation = []; + const segmentationsMap = new Map(); + + const representations = this.getSegmentationRepresentations(viewportId); + for (const representation of representations) { + const { segmentationId } = representation; + + if (!representation) { + continue; + } + + const { type } = representation; + + segmentationsMap.set(segmentationId, { + segmentationId, + type, + hydrated: true, + config: representation.config || {}, + }); + } + + // Check inside the removedDisplaySetAndRepresentationMaps to see if any of the representations are not hydrated + // const hydrationMap = this._segmentationRepresentationHydrationMaps.get(presentationId); + + // if (hydrationMap) { + // hydrationMap.forEach(rep => { + // segmentationsMap.set(rep.segmentationId, { + // segmentationId: rep.segmentationId, + // type: rep.type, + // hydrated: rep.hydrated, + // config: rep.config || {}, + // }); + // }); + // } + + // // Convert the Map to an array + segmentationPresentations.push(...segmentationsMap.values()); + + return segmentationPresentations; + } + + public getRepresentationsForSegmentation( + segmentationId: string + ): { viewportId: string; representations: any[] }[] { + const representations = + cstSegmentation.state.getSegmentationRepresentationsBySegmentationId(segmentationId); + + return representations; + } + + /** + * Retrieves segmentation representations (labelmap, contour, surface) based on specified criteria. + * + * @param viewportId - The ID of the viewport. + * @param specifier - An object containing optional `segmentationId` and `type` to filter the representations. + * @returns An array of `SegmentationRepresentation` matching the criteria, or an empty array if none are found. + * + * @remarks + * This method filters the segmentation representations according to the provided `specifier`: + * - **No `segmentationId` or `type` provided**: Returns all representations associated with the given `viewportId`. + * - **Only `segmentationId` provided**: Returns all representations with that `segmentationId`, regardless of `viewportId`. + * - **Only `type` provided**: Returns all representations of that `type` associated with the given `viewportId`. + * - **Both `segmentationId` and `type` provided**: Returns representations matching both criteria, regardless of `viewportId`. + */ + public getSegmentationRepresentations( + viewportId: string, + specifier: { + segmentationId?: string; + type?: SegmentationRepresentations; + } = {} + ): SegmentationRepresentation[] { + // Get all representations for the viewportId + const representations = cstSegmentation.state.getSegmentationRepresentations( + viewportId, + specifier + ); + + // Map to our SegmentationRepresentation type + const ohifRepresentations = representations.map(repr => + this._toOHIFSegmentationRepresentation(viewportId, repr) + ); + + return ohifRepresentations; + } + public destroy = () => { eventTarget.removeEventListener( csToolsEnums.Events.SEGMENTATION_MODIFIED, @@ -81,481 +226,198 @@ class SegmentationService extends PubSubService { ); eventTarget.removeEventListener( - csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, - this._onSegmentationDataModified + csToolsEnums.Events.SEGMENTATION_REMOVED, + this._onSegmentationModifiedFromSource ); - // remove the segmentations from the cornerstone - Object.keys(this.segmentations).forEach(segmentationId => { - this._removeSegmentationFromCornerstone(segmentationId); - }); + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, + this._onSegmentationDataModifiedFromSource + ); + + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_ADDED, + this._onSegmentationAddedFromSource + ); - this.segmentations = {}; this.listeners = {}; }; - /** - * Adds a new segment to the specified segmentation. - * @param segmentationId - The ID of the segmentation to add the segment to. - * @param config - An object containing the configuration options for the new segment. - * - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used. - * - toolGroupId: (optional) The ID of the tool group to associate the new segment with. If not provided, the first available tool group will be used. - * - properties: (optional) An object containing the properties of the new segment. - * - label: (optional) The label of the new segment. If not provided, a default label will be used. - * - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used. - * - opacity: (optional) The opacity of the new segment. If not provided, a default opacity will be used. - * - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default. - * - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default. - * - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default. - */ - public addSegment( - segmentationId: string, - config: { - segmentIndex?: number; - toolGroupId?: string; - properties?: { - label?: string; - color?: ohifTypes.RGB; - opacity?: number; - visibility?: boolean; - isLocked?: boolean; - active?: boolean; - }; - } = {} - ): void { - if (config?.segmentIndex === 0) { - throw new Error('Segment index 0 is reserved for "no label"'); - } - - const toolGroupId = config.toolGroupId ?? this._getApplicableToolGroupId(); - - const { segmentationRepresentationUID, segmentation } = this._getSegmentationInfo( + public async addSegmentationRepresentation( + viewportId: string, + { segmentationId, - toolGroupId - ); - - let segmentIndex = config.segmentIndex; - if (!segmentIndex) { - // grab the next available segment index - segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length; + type, + suppressEvents = false, + }: { + segmentationId: string; + type?: csToolsEnums.SegmentationRepresentations; + suppressEvents?: boolean; } - - if (this._getSegmentInfo(segmentation, segmentIndex)) { - throw new Error(`Segment ${segmentIndex} already exists`); - } - - const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex( - toolGroupId, - segmentationRepresentationUID, - segmentIndex - ); - - segmentation.segments[segmentIndex] = { - label: config.properties?.label ?? `Segment ${segmentIndex}`, - segmentIndex: segmentIndex, - color: [rgbaColor[0], rgbaColor[1], rgbaColor[2]], - opacity: rgbaColor[3], - isVisible: true, - isLocked: false, - }; - - segmentation.segmentCount++; - - // make the newly added segment the active segment - this._setActiveSegment(segmentationId, segmentIndex); - - const suppressEvents = true; - if (config.properties !== undefined) { - const { color: newColor, opacity, isLocked, visibility, active } = config.properties; - - if (newColor !== undefined) { - this._setSegmentColor(segmentationId, segmentIndex, newColor, toolGroupId, suppressEvents); - } - - if (opacity !== undefined) { - this._setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId, suppressEvents); - } - - if (visibility !== undefined) { - this._setSegmentVisibility( - segmentationId, - segmentIndex, - visibility, - toolGroupId, - suppressEvents - ); - } - - if (active === true) { - this._setActiveSegment(segmentationId, segmentIndex, suppressEvents); - } - - if (isLocked !== undefined) { - this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents); - } - } - - if (segmentation.activeSegmentIndex === null) { - this._setActiveSegment(segmentationId, segmentIndex, suppressEvents); - } - - // Todo: this includes non-hydrated segmentations which might not be - // persisted in the store - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - - public removeSegment(segmentationId: string, segmentIndex: number): void { + ): Promise { const segmentation = this.getSegmentation(segmentationId); + const csViewport = this.getAndValidateViewport(viewportId); + const colorLUTIndex = this._segmentationIdToColorLUTIndexMap.get(segmentationId); - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } + const defaultRepresentationType = csToolsEnums.SegmentationRepresentations.Labelmap; + let representationTypeToUse = type || defaultRepresentationType; + let isConverted = false; - if (segmentIndex === 0) { - throw new Error('Segment index 0 is reserved for "no label"'); - } + if (type === csToolsEnums.SegmentationRepresentations.Labelmap) { + const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType( + csViewport, + segmentation + ); - if (!this._getSegmentInfo(segmentation, segmentIndex)) { - return; - } - - segmentation.segmentCount--; - - segmentation.segments[segmentIndex] = null; - - // Get volume and delete the labels - // Todo: handle other segmentations other than labelmap - const labelmapVolume = this.getLabelmapVolume(segmentationId); - - const { dimensions } = labelmapVolume; - const scalarData = labelmapVolume.getScalarData(); - - // Set all values of this segment to zero and get which frames have been edited. - const frameLength = dimensions[0] * dimensions[1]; - const numFrames = dimensions[2]; - - let voxelIndex = 0; - - const modifiedFrames = new Set() as Set; - - for (let frame = 0; frame < numFrames; frame++) { - for (let p = 0; p < frameLength; p++) { - if (scalarData[voxelIndex] === segmentIndex) { - scalarData[voxelIndex] = 0; - modifiedFrames.add(frame); - } - - voxelIndex++; - } - } - - const modifiedFramesArray: number[] = Array.from(modifiedFrames); - - // Trigger texture update of modified segmentation frames. - cstSegmentation.triggerSegmentationEvents.triggerSegmentationDataModified( - segmentationId, - modifiedFramesArray - ); - - if (segmentation.activeSegmentIndex === segmentIndex) { - const segmentIndices = Object.keys(segmentation.segments); - - const newActiveSegmentIndex = segmentIndices.length ? Number(segmentIndices[0]) : 1; - - this._setActiveSegment(segmentationId, newActiveSegmentIndex, true); - } - - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - - public setSegmentVisibility( - segmentationId: string, - segmentIndex: number, - isVisible: boolean, - toolGroupId?: string, - suppressEvents = false - ): void { - this._setSegmentVisibility( - segmentationId, - segmentIndex, - isVisible, - toolGroupId, - suppressEvents - ); - } - - public setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void { - const suppressEvents = false; - this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents); - } - - /** - * Toggles the locked state of a segment in a segmentation. - * @param segmentationId - The ID of the segmentation. - * @param segmentIndex - The index of the segment to toggle. - */ - public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void { - const segmentation = this.getSegmentation(segmentationId); - const segment = this._getSegmentInfo(segmentation, segmentIndex); - const isLocked = !segment.isLocked; - this._setSegmentLocked(segmentationId, segmentIndex, isLocked); - } - - public setSegmentColor( - segmentationId: string, - segmentIndex: number, - color: ohifTypes.RGB, - toolGroupId?: string - ): void { - this._setSegmentColor(segmentationId, segmentIndex, color, toolGroupId); - } - - public setSegmentRGBA = ( - segmentationId: string, - segmentIndex: number, - rgbaColor: cstTypes.Color, - toolGroupId?: string - ): void => { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - const suppressEvents = true; - this._setSegmentOpacity( - segmentationId, - segmentIndex, - rgbaColor[3], - toolGroupId, - suppressEvents - ); - - this._setSegmentColor( - segmentationId, - segmentIndex, - [rgbaColor[0], rgbaColor[1], rgbaColor[2]], - toolGroupId, - suppressEvents - ); - - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - }; - - public setSegmentOpacity( - segmentationId: string, - segmentIndex: number, - opacity: number, - toolGroupId?: string - ): void { - this._setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId); - } - - public setActiveSegmentationForToolGroup(segmentationId: string, toolGroupId?: string): void { - toolGroupId = toolGroupId ?? this._getApplicableToolGroupId(); - - const suppressEvents = false; - this._setActiveSegmentationForToolGroup(segmentationId, toolGroupId, suppressEvents); - } - - public setActiveSegment(segmentationId: string, segmentIndex: number): void { - this._setActiveSegment(segmentationId, segmentIndex, false); - } - - /** - * Get all segmentations. - * - * * @param filterNonHydratedSegmentations - If true, only return hydrated segmentations - * hydrated segmentations are those that have been loaded and persisted - * in the state, but non hydrated segmentations are those that are - * only created for the SEG displayset (SEG viewport) and the user might not - * have loaded them yet fully. - * - - * @return Array of segmentations - */ - public getSegmentations(filterNonHydratedSegmentations = true): Segmentation[] { - const segmentations = this._getSegmentations(); - - return ( - segmentations && - segmentations.filter(segmentation => { - return !filterNonHydratedSegmentations || segmentation.hydrated; - }) - ); - } - - private _getSegmentations(): Segmentation[] { - const segmentations = this.arrayOfObjects(this.segmentations); - return segmentations && segmentations.map(m => this.segmentations[Object.keys(m)[0]]); - } - - public getActiveSegmentation(): Segmentation { - const segmentations = this.getSegmentations(); - - return segmentations.find(segmentation => segmentation.isActive); - } - - public getActiveSegment() { - const activeSegmentation = this.getActiveSegmentation(); - const { activeSegmentIndex, segments } = activeSegmentation; - - if (activeSegmentIndex === null) { - return; - } - - return segments[activeSegmentIndex]; - } - - /** - * Get specific segmentation by its id. - * - * @param segmentationId If of the segmentation - * @return segmentation instance - */ - public getSegmentation(segmentationId: string): Segmentation { - return this.segmentations[segmentationId]; - } - - public addOrUpdateSegmentation( - segmentation: Segmentation, - suppressEvents = false, - notYetUpdatedAtSource = false - ): string { - const { id: segmentationId } = segmentation; - let cachedSegmentation = this.segmentations[segmentationId]; - if (cachedSegmentation) { - // Update the segmentation (mostly for assigning metadata/labels) - Object.assign(cachedSegmentation, segmentation); - - this._updateCornerstoneSegmentations({ + ({ representationTypeToUse, isConverted } = await this.handleViewportConversion( + isVolumeViewport, + isVolumeSegmentation, + csViewport, + segmentation, + viewportId, segmentationId, - notYetUpdatedAtSource, - }); - - if (!suppressEvents) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation: cachedSegmentation, - }); - } - - return segmentationId; + representationTypeToUse + )); } - const representationType = segmentation.type; - const representationData = segmentation.representationData[representationType]; - cstSegmentation.addSegmentations([ - { - segmentationId, - representation: { - type: representationType, - data: { - ...representationData, - }, - }, - }, - ]); - - this.segmentations[segmentationId] = { - ...segmentation, - label: segmentation.label || '', - segments: segmentation.segments || [null], - activeSegmentIndex: segmentation.activeSegmentIndex ?? null, - segmentCount: segmentation.segmentCount ?? 0, - isActive: false, - isVisible: true, - }; - - cachedSegmentation = this.segmentations[segmentationId]; - - this._updateCornerstoneSegmentations({ + await this._addSegmentationRepresentation( + viewportId, segmentationId, - notYetUpdatedAtSource: true, - }); + representationTypeToUse, + colorLUTIndex, + isConverted + ); if (!suppressEvents) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, { - segmentation: cachedSegmentation, - }); + this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, { segmentationId }); + } + } + + /** + * Creates an labelmap segmentation for a given display set + * + * @param displaySet - The display set to create the segmentation for. + * @param options - Optional parameters for creating the segmentation. + * @param options.segmentationId - Custom segmentation ID. If not provided, a UUID will be generated. + * @param options.FrameOfReferenceUID - Frame of reference UID for the segmentation. + * @param options.label - Label for the segmentation. + * @returns A promise that resolves to the created segmentation ID. + */ + public async createLabelmapForDisplaySet( + displaySet: AppTypes.DisplaySet, + options?: { + segmentationId?: string; + segments?: { [segmentIndex: number]: Partial }; + FrameOfReferenceUID?: string; + label?: string; + } + ): Promise { + // Todo: random does not makes sense, make this better, like + // labelmap 1, 2, 3 etc + const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`; + + const isDynamicVolume = displaySet.isDynamicVolume; + + let referenceImageIds = displaySet.imageIds; + if (isDynamicVolume) { + // get the middle timepoint for referenceImageIds + const timePoints = displaySet.dynamicVolumeInfo.timePoints; + const middleTimePoint = timePoints[Math.floor(timePoints.length / 2)]; + referenceImageIds = middleTimePoint; } - return cachedSegmentation.id; + const derivedImages = await imageLoader.createAndCacheDerivedLabelmapImages(referenceImageIds); + + const segs = this.getSegmentations(); + const label = options.label || `Segmentation ${segs.length + 1}`; + + const segImageIds = derivedImages.map(image => image.imageId); + + const segmentationPublicInput: cstTypes.SegmentationPublicInput = { + segmentationId, + representation: { + type: LABELMAP, + data: { + imageIds: segImageIds, + referencedVolumeId: this._getVolumeIdForDisplaySet(displaySet), + referencedImageIds: referenceImageIds, + }, + }, + config: { + label, + segments: + options.segments && Object.keys(options.segments).length > 0 + ? options.segments + : { + 1: { + label: 'Segment 1', + active: true, + }, + }, + cachedStats: { + info: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`, + }, + }, + }; + + this.addOrUpdateSegmentation(segmentationPublicInput); + return segmentationId; } public async createSegmentationForSEGDisplaySet( segDisplaySet, - segmentationId?: string, - suppressEvents = false + options: { + segmentationId?: string; + type: SegmentationRepresentations; + } = { + type: LABELMAP, + } ): Promise { - // Todo: we only support creating labelmap for SEG displaySets for now - const representationType = LABELMAP; + const { type } = options; + let { segmentationId } = options; + const { labelmapBufferArray } = segDisplaySet; + + if (type !== LABELMAP) { + throw new Error('Only labelmap type is supported for SEG display sets right now'); + } + + if (!labelmapBufferArray) { + throw new Error('SEG reading failed'); + } segmentationId = segmentationId ?? segDisplaySet.displaySetInstanceUID; + const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); - const defaultScheme = this._getDefaultSegmentationScheme(); + const images = referencedDisplaySet.instances; - const segmentation: Segmentation = { - ...defaultScheme, - id: segmentationId, - displaySetInstanceUID: segDisplaySet.displaySetInstanceUID, - type: representationType, - label: segDisplaySet.SeriesDescription, - representationData: { - [LABELMAP]: { - volumeId: segmentationId, - referencedVolumeId: segDisplaySet.referencedVolumeId, - }, - }, - }; - - const labelmap = this.getLabelmapVolume(segmentationId); - const cachedSegmentation = this.getSegmentation(segmentationId); - if (labelmap && cachedSegmentation) { - // if the labelmap with the same segmentationId already exists, we can - // just assume that the segmentation is already created and move on with - // updating the state - return this.addOrUpdateSegmentation( - Object.assign(segmentation, cachedSegmentation), - suppressEvents - ); + if (!images.length) { + throw new Error('No instances were provided for the referenced display set of the SEG'); } - const { labelmapBufferArray, referencedVolumeId } = segDisplaySet; + const imageIds = images.map(image => image.imageId); - if (!labelmapBufferArray || !referencedVolumeId) { - throw new Error('No labelmapBufferArray or referencedVolumeId found for the SEG displaySet'); - } + const derivedSegmentationImages = await imageLoader.createAndCacheDerivedLabelmapImages( + imageIds as string[] + ); - // if the labelmap doesn't exist, we need to create it first from the - // DICOM SEG displaySet data - const referencedVolume = cache.getVolume(referencedVolumeId); - - if (!referencedVolume) { - throw new Error(`No volume found for referencedVolumeId: ${referencedVolumeId}`); - } - - // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so - // it is easily compressible in worker thread. - const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, { - volumeId: segmentationId, - targetBuffer: { - type: 'Uint8Array', - sharedArrayBuffer: window.SharedArrayBuffer, - }, - }); - const derivedVolumeScalarData = derivedVolume.getScalarData(); + segDisplaySet.images = derivedSegmentationImages; const segmentsInfo = segDisplaySet.segMetadata.data; - derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0])); - segmentation.segments = segmentsInfo.map((segmentInfo, segmentIndex) => { - if (segmentIndex === 0) { + const segments: { [segmentIndex: string]: cstTypes.Segment } = {}; + const colorLUT = []; + + segmentsInfo.forEach((segmentInfo, index) => { + if (index === 0) { + colorLUT.push([0, 0, 0, 0]); return; } @@ -569,115 +431,147 @@ class SegmentationService extends PubSubService { rgba, } = segmentInfo; - const { x, y, z } = segDisplaySet.centroids.get(segmentIndex) || { x: 0, y: 0, z: 0 }; - const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]); + colorLUT.push(rgba); - segmentation.cachedStats = { - ...segmentation.cachedStats, - segmentCenter: { - ...segmentation.cachedStats.segmentCenter, - [segmentIndex]: { - center: { - image: [x, y, z], - world: centerWorld, - }, - modifiedTime: segDisplaySet.SeriesDate, - }, - }, - }; + const segmentIndex = Number(SegmentNumber); - return { + const imageCentroidXYZ = segDisplaySet.centroids.get(index).image || { x: 0, y: 0, z: 0 }; + const worldCentroidXYZ = segDisplaySet.centroids.get(index).world || { x: 0, y: 0, z: 0 }; + + segments[segmentIndex] = { + segmentIndex, label: SegmentLabel || `Segment ${SegmentNumber}`, - segmentIndex: Number(SegmentNumber), - category: SegmentedPropertyCategoryCodeSequence - ? SegmentedPropertyCategoryCodeSequence.CodeMeaning - : '', - type: SegmentedPropertyTypeCodeSequence - ? SegmentedPropertyTypeCodeSequence.CodeMeaning - : '', - algorithmType: SegmentAlgorithmType, - algorithmName: SegmentAlgorithmName, - color: rgba, - opacity: 255, - isVisible: true, - isLocked: false, + locked: false, + active: false, + cachedStats: { + center: { + image: [imageCentroidXYZ.x, imageCentroidXYZ.y, imageCentroidXYZ.z], + world: [worldCentroidXYZ.x, worldCentroidXYZ.y, worldCentroidXYZ.z], + }, + modifiedTime: segDisplaySet.SeriesDate, + category: SegmentedPropertyCategoryCodeSequence + ? SegmentedPropertyCategoryCodeSequence.CodeMeaning + : '', + type: SegmentedPropertyTypeCodeSequence + ? SegmentedPropertyTypeCodeSequence.CodeMeaning + : '', + algorithmType: SegmentAlgorithmType, + algorithmName: SegmentAlgorithmName, + }, }; }); - segmentation.segmentCount = segmentsInfo.length - 1; + // get next color lut index + const colorLUTIndex = getNextColorLUTIndex(); + addColorLUT(colorLUT, colorLUTIndex); + this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); - segDisplaySet.isLoaded = true; + // now we need to chop the volume array into chunks and set the scalar data for each derived segmentation image + const volumeScalarData = new Uint8Array(labelmapBufferArray[0]); + + // We should parse the segmentation as separate slices to support overlapping segments. + // This parsing should occur in the CornerstoneJS library adapters. + // For now, we use the volume returned from the library and chop it here. + for (let i = 0; i < derivedSegmentationImages.length; i++) { + const voxelManager = derivedSegmentationImages[i] + .voxelManager as csTypes.IVoxelManager; + const scalarData = voxelManager.getScalarData(); + scalarData.set(volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length)); + voxelManager.setScalarData(scalarData); + } this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { segmentationId, segDisplaySet, }); - return this.addOrUpdateSegmentation(segmentation, suppressEvents); + const seg: cstTypes.SegmentationPublicInput = { + segmentationId, + representation: { + type: LABELMAP, + data: { + imageIds: derivedSegmentationImages.map(image => image.imageId), + referencedVolumeId: this._getVolumeIdForDisplaySet(referencedDisplaySet), + referencedImageIds: imageIds as string[], + }, + }, + config: { + label: segDisplaySet.SeriesDescription, + segments, + }, + }; + + segDisplaySet.isLoaded = true; + + this.addOrUpdateSegmentation(seg); + + return segmentationId; } public async createSegmentationForRTDisplaySet( rtDisplaySet, - segmentationId?: string, - suppressEvents = false + options: { + segmentationId?: string; + type: SegmentationRepresentations; + } = { + type: CONTOUR, + } ): Promise { - // Todo: we currently only have support for contour representation for initial - // RT display - const representationType = CONTOUR; + const { type } = options; + let { segmentationId } = options; + + // Currently, only contour representation is supported for RT display + if (type !== CONTOUR) { + throw new Error('Only contour type is supported for RT display sets right now'); + } + + // Assign segmentationId if not provided segmentationId = segmentationId ?? rtDisplaySet.displaySetInstanceUID; const { structureSet } = rtDisplaySet; if (!structureSet) { throw new Error( - 'To create the contours from RT displaySet, the displaySet should be loaded first, you can perform rtDisplaySet.load() before calling this method.' + 'To create the contours from RT displaySet, the displaySet should be loaded first. You can perform rtDisplaySet.load() before calling this method.' ); } - const defaultScheme = this._getDefaultSegmentationScheme(); const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID; + // Map ROI contours to RT Struct Data const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID); - // sort by segmentIndex + // Sort by segmentIndex for consistency allRTStructData.sort((a, b) => a.segmentIndex - b.segmentIndex); const geometryIds = allRTStructData.map(({ geometryId }) => geometryId); - const segmentation: Segmentation = { - ...defaultScheme, - id: segmentationId, - displaySetInstanceUID: rtDisplaySetUID, - type: representationType, - label: rtDisplaySet.SeriesDescription, - representationData: { - [CONTOUR]: { + // Initialize SegmentationPublicInput similar to SEG function + const segmentation: cstTypes.SegmentationPublicInput = { + segmentationId, + representation: { + type: CONTOUR, + data: { geometryIds, }, }, + config: { + label: rtDisplaySet.SeriesDescription, + }, }; - const cachedSegmentation = this.getSegmentation(segmentationId); - - if (cachedSegmentation) { - // if the labelmap with the same segmentationId already exists, we can - // just assume that the segmentation is already created and move on with - // updating the state - return this.addOrUpdateSegmentation( - Object.assign(segmentation, cachedSegmentation), - suppressEvents - ); - } - if (!structureSet.ROIContours?.length) { throw new Error( 'The structureSet does not contain any ROIContours. Please ensure the structureSet is loaded first.' ); } - const segmentsCachedStats = {}; - const initializeContour = async rtStructData => { + + const segments: { [segmentIndex: string]: cstTypes.Segment } = {}; + let segmentsCachedStats = {}; + + // Process each segment similarly to the SEG function + for (const rtStructData of allRTStructData) { const { data, id, color, segmentIndex, geometryId } = rtStructData; - // catch error instead of failing to allow loading to continue try { const geometry = await geometryLoader.createAndCacheGeometry(geometryId, { geometryData: { @@ -690,228 +584,552 @@ class SegmentationService extends PubSubService { type: csEnums.GeometryType.CONTOUR, }); - const contourSet = geometry.data; - const centroid = contourSet.getCentroid(); + const contourSet = geometry.data as csTypes.IContourSet; + const centroid = contourSet.centroid; - segmentsCachedStats[segmentIndex] = { + segmentsCachedStats = { center: { world: centroid }, - modifiedTime: rtDisplaySet.SeriesDate, // we use the SeriesDate as the modifiedTime since this is the first time we are creating the segmentation + modifiedTime: rtDisplaySet.SeriesDate, // Using SeriesDate as modifiedTime }; - segmentation.segments[segmentIndex] = { + segments[segmentIndex] = { label: id, segmentIndex, - color, - ...SEGMENT_CONSTANT, + cachedStats: segmentsCachedStats, + locked: false, + active: false, }; + // Broadcast segment loading progress const numInitialized = Object.keys(segmentsCachedStats).length; - - // Calculate percentage completed const percentComplete = Math.round((numInitialized / allRTStructData.length) * 100); - this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, { percentComplete, - // Note: this is not the geometryIds length since there might be - // some missing ROINumbers numSegments: allRTStructData.length, }); } catch (e) { - console.warn(e); + console.warn(`Error initializing contour for segment ${segmentIndex}:`, e); + continue; // Continue processing other segments even if one fails } - }; - - const promiseArray = []; - - for (let i = 0; i < allRTStructData.length; i++) { - const promise = new Promise((resolve, reject) => { - setTimeout(() => { - initializeContour(allRTStructData[i]).then(() => { - resolve(); - }); - }, 0); - }); - - promiseArray.push(promise); } - await Promise.all(promiseArray); - - segmentation.segmentCount = allRTStructData.length; - rtDisplaySet.isLoaded = true; - - segmentation.cachedStats = { - ...segmentation.cachedStats, - segmentCenter: { - ...segmentation.cachedStats.segmentCenter, - ...segmentsCachedStats, - }, - }; + // Assign processed segments to segmentation config + segmentation.config.segments = segments; + // Broadcast segmentation loading complete event this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { segmentationId, rtDisplaySet, }); - return this.addOrUpdateSegmentation(segmentation, suppressEvents); + // Mark the RT display set as loaded + rtDisplaySet.isLoaded = true; + + // Add or update the segmentation in the state + this.addOrUpdateSegmentation(segmentation); + + return segmentationId; } - // Todo: this should not run on the main thread - public calculateCentroids = ( - segmentationId: string, - segmentIndex?: number - ): Map => { - const segmentation = this.getSegmentation(segmentationId); - const volume = this.getLabelmapVolume(segmentationId); - const { dimensions, imageData } = volume; - const scalarData = volume.getScalarData(); - const [dimX, dimY, numFrames] = dimensions; - const frameLength = dimX * dimY; + /** + * Adds or updates a segmentation in the state + * @param segmentationId - The ID of the segmentation to add or update + * @param data - The data to add or update the segmentation with + * + * @remarks + * This method handles the addition or update of a segmentation in the state. + * If the segmentation already exists, it updates the existing segmentation. + * If the segmentation does not exist, it adds a new segmentation. + */ + public addOrUpdateSegmentation( + data: cstTypes.SegmentationPublicInput | Partial + ) { + const segmentationId = data.segmentationId; + const existingSegmentation = cstSegmentation.state.getSegmentation(segmentationId); - const segmentIndices = segmentIndex - ? [segmentIndex] - : segmentation.segments - .filter(segment => segment?.segmentIndex) - .map(segment => segment.segmentIndex); + if (existingSegmentation) { + // Update the existing segmentation + this.updateSegmentationInSource(segmentationId, data as Partial); + } else { + // Add a new segmentation + this.addSegmentationToSource(data as cstTypes.SegmentationPublicInput); + } + } - const segmentIndicesSet = new Set(segmentIndices); + public setActiveSegmentation(viewportId: string, segmentationId: string): void { + cstSegmentation.activeSegmentation.setActiveSegmentation(viewportId, segmentationId); + } - const centroids = new Map(); - for (const index of segmentIndicesSet) { - centroids.set(index, { x: 0, y: 0, z: 0, count: 0 }); + /** + * Gets the active segmentation for a viewport + * @param viewportId - The ID of the viewport to get the active segmentation for + * @returns The active segmentation object, or null if no segmentation is active + * + * @remarks + * This method retrieves the currently active segmentation for the specified viewport. + * The active segmentation is the one that is currently selected for editing operations. + * Returns null if no segmentation is active in the viewport. + */ + public getActiveSegmentation(viewportId: string): cstTypes.Segmentation | null { + return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId); + } + + /** + * Gets the active segment from the active segmentation in a viewport + * @param viewportId - The ID of the viewport to get the active segment from + * @returns The active segment object, or undefined if no segment is active + * + * @remarks + * This method retrieves the currently active segment from the active segmentation + * in the specified viewport. The active segment is the one that is currently + * selected for editing operations. Returns undefined if no segment is active or + * if there is no active segmentation. + */ + public getActiveSegment(viewportId: string): cstTypes.Segment | undefined { + const activeSegmentation = this.getActiveSegmentation(viewportId); + + if (!activeSegmentation) { + return; } - let voxelIndex = 0; - for (let frame = 0; frame < numFrames; frame++) { - for (let p = 0; p < frameLength; p++) { - const segmentIndex = scalarData[voxelIndex++]; - if (segmentIndicesSet.has(segmentIndex)) { - const centroid = centroids.get(segmentIndex); - centroid.x += p % dimX; - centroid.y += (p / dimX) | 0; - centroid.z += frame; - centroid.count++; - } + const { segments } = activeSegmentation; + + let activeSegment; + for (const segment of Object.values(segments)) { + if (segment.active) { + activeSegment = segment; + break; } } - const result = new Map(); - for (const [index, centroid] of centroids) { - const count = centroid.count; - const normalizedCentroid = { - x: centroid.x / count, - y: centroid.y / count, - z: centroid.z / count, - }; - normalizedCentroid.world = imageData.indexToWorld([ - normalizedCentroid.x, - normalizedCentroid.y, - normalizedCentroid.z, - ]); - result.set(index, normalizedCentroid); - } + return activeSegment; + } - this.setCentroids(segmentationId, result); - return result; + public hasCustomStyles(specifier: { + viewportId: string; + segmentationId: string; + type: SegmentationRepresentations; + }): boolean { + return cstSegmentation.config.style.hasCustomStyle(specifier); + } + + public getStyle = (specifier: { + viewportId: string; + segmentationId: string; + type: SegmentationRepresentations; + segmentIndex?: number; + }) => { + const style = cstSegmentation.config.style.getStyle(specifier); + + return style; }; - private setCentroids = ( - segmentationId: string, - centroids: Map - ): void => { - const segmentation = this.getSegmentation(segmentationId); - const imageData = this.getLabelmapVolume(segmentationId).imageData; // Assuming this method returns imageData + public setStyle = ( + specifier: { + type: SegmentationRepresentations; + viewportId?: string; + segmentationId?: string; + segmentIndex?: number; + }, + style: LabelmapStyle | ContourStyle | SurfaceStyle + ) => { + cstSegmentation.config.style.setStyle(specifier, style); + }; - if (!segmentation.cachedStats) { - segmentation.cachedStats = { segmentCenter: {} }; - } else if (!segmentation.cachedStats.segmentCenter) { - segmentation.cachedStats.segmentCenter = {}; + public resetToGlobalStyle = () => { + cstSegmentation.config.style.resetToGlobalStyle(); + }; + + /** + * Adds a new segment to the specified segmentation. + * @param segmentationId - The ID of the segmentation to add the segment to. + * @param viewportId: The ID of the viewport to add the segment to, it is used to get the representation, if it is not + * provided, the first available representation for the segmentationId will be used. + * @param config - An object containing the configuration options for the new segment. + * - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used. + * - properties: (optional) An object containing the properties of the new segment. + * - label: (optional) The label of the new segment. If not provided, a default label will be used. + * - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used. + * - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default. + * - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default. + * - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default. + */ + public addSegment( + segmentationId: string, + config: { + segmentIndex?: number; + label?: string; + isLocked?: boolean; + active?: boolean; + color?: csTypes.Color; // Add color type + visibility?: boolean; // Add visibility option + } = {} + ): void { + if (config?.segmentIndex === 0) { + throw new Error('Segment index 0 is reserved for "no label"'); } - for (const [segmentIndex, centroid] of centroids) { - let world = centroid.world; + const csSegmentation = this.getCornerstoneSegmentation(segmentationId); - // If world coordinates are not provided, calculate them - if (!world || world.length === 0) { - world = imageData.indexToWorld(centroid.image); - } + let segmentIndex = config.segmentIndex; + if (!segmentIndex) { + // grab the next available segment index based on the object keys, + // so basically get the highest segment index value + 1 + segmentIndex = Math.max(...Object.keys(csSegmentation.segments).map(Number)) + 1; + } - segmentation.cachedStats.segmentCenter[segmentIndex] = { - center: { - image: centroid.image, - world: world, + // update the segmentation + if (!config.label) { + config.label = `Segment ${segmentIndex}`; + } + + const currentSegments = csSegmentation.segments; + + cstSegmentation.updateSegmentations([ + { + segmentationId, + payload: { + segments: { + ...currentSegments, + [segmentIndex]: { + ...currentSegments[segmentIndex], + segmentIndex, + cachedStats: {}, + locked: false, + ...config, + }, + }, }, - }; + }, + ]); + + this.setActiveSegment(segmentationId, segmentIndex); + + // Apply additional configurations + if (config.isLocked !== undefined) { + this._setSegmentLockedStatus(segmentationId, segmentIndex, config.isLocked); } - this.addOrUpdateSegmentation(segmentation, true, true); + // Get all viewports that have this segmentation + const viewportIds = this.getViewportIdsWithSegmentation(segmentationId); + + viewportIds.forEach(viewportId => { + // Set color if provided + if (config.color !== undefined) { + this.setSegmentColor(viewportId, segmentationId, segmentIndex, config.color); + } + + // Set visibility if provided + if (config.visibility !== undefined) { + this.setSegmentVisibility(viewportId, segmentationId, segmentIndex, config.visibility); + } + }); + } + + /** + * Removes a segment from a segmentation and updates the active segment index if necessary. + * + * @param segmentationId - The ID of the segmentation containing the segment to remove. + * @param segmentIndex - The index of the segment to remove. + * + * @remarks + * This method performs the following actions: + * 1. Clears the segment value in the Cornerstone segmentation. + * 2. Updates all related segmentation representations to remove the segment. + * 3. If the removed segment was the active segment, it updates the active segment index. + * + */ + public removeSegment(segmentationId: string, segmentIndex: number): void { + cstSegmentation.removeSegment(segmentationId, segmentIndex); + } + + public setSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + type?: SegmentationRepresentations + ): void { + this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, isVisible, type); + } + + /** + * Sets the locked status of a segment in a segmentation. + * + * @param segmentationId - The ID of the segmentation containing the segment. + * @param segmentIndex - The index of the segment to set the locked status for. + * @param isLocked - The new locked status of the segment. + * + * @remarks + * This method updates the locked status of a specific segment within a segmentation. + * A locked segment cannot be modified or edited. + */ + public setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void { + this._setSegmentLockedStatus(segmentationId, segmentIndex, isLocked); + } + + /** + * Toggles the locked state of a segment in a segmentation. + * @param segmentationId - The ID of the segmentation. + * @param segmentIndex - The index of the segment to toggle. + */ + public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void { + const isLocked = cstSegmentation.segmentLocking.isSegmentIndexLocked( + segmentationId, + segmentIndex + ); + this._setSegmentLockedStatus(segmentationId, segmentIndex, !isLocked); + } + + public toggleSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + type: SegmentationRepresentations + ): void { + const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility( + viewportId, + { + segmentationId, + type, + }, + segmentIndex + ); + this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, !isVisible, type); + } + + /** + * Sets the color of a specific segment in a segmentation. + * + * @param viewportId - The ID of the viewport containing the segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to set the color for + * @param color - The new color to apply to the segment as an array of RGBA values + * + * @remarks + * This method updates the color of a specific segment within a segmentation. + * The color parameter should be an array of 4 numbers representing RGBA values. + */ + public setSegmentColor( + viewportId: string, + segmentationId: string, + segmentIndex: number, + color: csTypes.Color + ): void { + cstSegmentation.config.color.setSegmentIndexColor( + viewportId, + segmentationId, + segmentIndex, + color + ); + } + + /** + * Gets the current color of a specific segment in a segmentation. + * + * @param viewportId - The ID of the viewport containing the segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to get the color for + * @returns An array of 4 numbers representing the RGBA color values of the segment + * + * @remarks + * This method retrieves the current color of a specific segment within a segmentation. + * The returned color is an array of 4 numbers representing RGBA values. + */ + public getSegmentColor(viewportId: string, segmentationId: string, segmentIndex: number) { + return cstSegmentation.config.color.getSegmentIndexColor( + viewportId, + segmentationId, + segmentIndex + ); + } + + /** + * Gets the labelmap volume for a segmentation + * @param segmentationId - The ID of the segmentation to get the labelmap volume for + * @returns The labelmap volume for the segmentation, or null if not found + * + * @remarks + * This method retrieves the labelmap volume data for a specific segmentation. + * The labelmap volume contains the actual segmentation data in the form of a 3D volume. + * Returns null if the segmentation does not have valid labelmap volume data. + */ + public getLabelmapVolume(segmentationId: string) { + const csSegmentation = cstSegmentation.state.getSegmentation(segmentationId); + const labelmapData = csSegmentation.representationData[ + SegmentationRepresentations.Labelmap + ] as cstTypes.LabelmapToolOperationDataVolume; + + if (!labelmapData || !labelmapData.volumeId) { + return null; + } + + const { volumeId } = labelmapData; + const labelmapVolume = cache.getVolume(volumeId); + + return labelmapVolume; + } + + /** + * Sets the label for a specific segment in a segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to set the label for + * @param label - The new label to apply to the segment + * + * @remarks + * This method updates the text label of a specific segment within a segmentation. + * The label is used to identify and describe the segment in the UI. + */ + public setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) { + this._setSegmentLabel(segmentationId, segmentIndex, label); + } + + /** + * Sets the active segment for a segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to set as active + * + * @remarks + * This method updates which segment is considered "active" within a segmentation. + * The active segment is typically highlighted and available for editing operations. + */ + public setActiveSegment(segmentationId: string, segmentIndex: number): void { + this._setActiveSegment(segmentationId, segmentIndex); + } + + /** + * Controls whether inactive segmentations should be rendered in a viewport + * @param viewportId - The ID of the viewport to update + * @param renderInactive - Whether inactive segmentations should be rendered + * + * @remarks + * This method configures if segmentations that are not currently active + * should still be visible in the specified viewport. This can be useful + * for comparing or viewing multiple segmentations simultaneously. + */ + public setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void { + cstSegmentation.config.style.setRenderInactiveSegmentations(viewportId, renderInactive); + } + + /** + * Gets whether inactive segmentations are being rendered for a viewport + * @param viewportId - The ID of the viewport to check + * @returns boolean indicating if inactive segmentations are rendered + * + * @remarks + * This method retrieves the current rendering state for inactive segmentations + * in the specified viewport. Returns true if inactive segmentations are visible. + */ + public getRenderInactiveSegmentations(viewportId: string): boolean { + return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId); + } + + /** + * Toggles the visibility of a segmentation in the state, and broadcasts the event. + * Note: this method does not update the segmentation state in the source. It only + * updates the state, and there should be separate listeners for that. + * @param ids segmentation ids + */ + public toggleSegmentationRepresentationVisibility = ( + viewportId: string, + { segmentationId, type }: { segmentationId: string; type: SegmentationRepresentations } + ): void => { + this._toggleSegmentationRepresentationVisibility(viewportId, segmentationId, type); }; + public getViewportIdsWithSegmentation = (segmentationId: string): string[] => { + const viewportIds = cstSegmentation.state.getViewportIdsWithSegmentation(segmentationId); + return viewportIds; + }; + + /** + * Clears segmentation representations from the viewport. + * Unlike removeSegmentationRepresentations, this doesn't update + * removed display set and representation maps. + * We track removed segmentations manually to avoid re-adding them + * when the display set is added again. + * @param viewportId - The viewport ID to clear segmentation representations from. + */ + public clearSegmentationRepresentations(viewportId: string): void { + this.removeSegmentationRepresentations(viewportId); + } + + /** + * Completely removes a segmentation from the state + * @param segmentationId - The ID of the segmentation to remove. + */ + public remove(segmentationId: string): void { + cstSegmentation.state.removeSegmentation(segmentationId); + } + + public removeAllSegmentations(): void { + cstSegmentation.state.removeAllSegmentations(); + } + + /** + * It removes the segmentation representations from the viewport. + * @param viewportId - The viewport id to remove the segmentation representations from. + * @param specifier - The specifier to remove the segmentation representations. + * + * @remarks + * If no specifier is provided, all segmentation representations for the viewport are removed. + * If a segmentationId specifier is provided, only the segmentation representation with the specified segmentationId and type are removed. + * If a type specifier is provided, only the segmentation representation with the specified type are removed. + * If both a segmentationId and type specifier are provided, only the segmentation representation with the specified segmentationId and type are removed. + */ + public removeSegmentationRepresentations( + viewportId: string, + specifier: { + segmentationId?: string; + type?: SegmentationRepresentations; + } = {} + ): void { + cstSegmentation.removeSegmentationRepresentations(viewportId, specifier); + } + public jumpToSegmentCenter( segmentationId: string, segmentIndex: number, - toolGroupId?: string, + viewportId?: string, highlightAlpha = 0.9, highlightSegment = true, animationLength = 750, highlightHideOthers = false, highlightFunctionType = 'ease-in-out' // todo: make animation functions configurable from outside ): void { - const { toolGroupService } = this.servicesManager.services; const center = this._getSegmentCenter(segmentationId, segmentIndex); - if (!center?.world) { + if (!center) { return; } - const { world } = center; + const { world } = center as { world: csTypes.Point3 }; - // todo: generalize - toolGroupId = toolGroupId || this._getToolGroupIdsWithSegmentation(segmentationId); + // need to find which viewports are displaying the segmentation + const viewportIds = viewportId + ? [viewportId] + : this.getViewportIdsWithSegmentation(segmentationId); - const toolGroups = []; + viewportIds.forEach(viewportId => { + const { viewport } = getEnabledElementByViewportId(viewportId); + viewport.jumpToWorld(world); - if (Array.isArray(toolGroupId)) { - toolGroupId.forEach(toolGroup => { - toolGroups.push(toolGroupService.getToolGroup(toolGroup)); - }); - } else { - toolGroups.push(toolGroupService.getToolGroup(toolGroupId)); - } - - toolGroups.forEach(toolGroup => { - const viewportsInfo = toolGroup.getViewportsInfo(); - - // @ts-ignore - for (const { viewportId, renderingEngineId } of viewportsInfo) { - const { viewport } = getEnabledElementByIds(viewportId, renderingEngineId); - if (viewport instanceof StackViewport) { - const { element } = viewport; - const index = csUtils.getClosestStackImageIndexForPoint(world, viewport) - cstUtils.viewport.jumpToSlice(element, { imageIndex: index }) - } else { - cstUtils.viewport.jumpToWorld(viewport, world); - } - } - - if (highlightSegment) { + highlightSegment && this.highlightSegment( segmentationId, segmentIndex, - toolGroup.id, + viewportId, highlightAlpha, animationLength, - highlightHideOthers, - highlightFunctionType + highlightHideOthers ); - } }); } public highlightSegment( segmentationId: string, segmentIndex: number, - toolGroupId?: string, + viewportId?: string, alpha = 0.9, animationLength = 750, hideOthers = true, @@ -921,245 +1139,381 @@ class SegmentationService extends PubSubService { clearInterval(this.highlightIntervalId); } - const segmentation = this.getSegmentation(segmentationId); - toolGroupId = toolGroupId ?? this._getApplicableToolGroupId(); + const csSegmentation = this.getCornerstoneSegmentation(segmentationId); - const segmentationRepresentation = this._getSegmentationRepresentation( - segmentationId, - toolGroupId - ); + const viewportIds = viewportId + ? [viewportId] + : this.getViewportIdsWithSegmentation(segmentationId); - const { type } = segmentationRepresentation; - const { segments } = segmentation; + viewportIds.forEach(viewportId => { + const segmentationRepresentation = this.getSegmentationRepresentations(viewportId, { + segmentationId, + }); - const highlightFn = - type === LABELMAP ? this._highlightLabelmap.bind(this) : this._highlightContour.bind(this); + const representation = segmentationRepresentation[0]; + const { type } = representation; + const segments = csSegmentation.segments; - const adjustedAlpha = type === LABELMAP ? alpha : 1 - alpha; + const highlightFn = + type === LABELMAP ? this._highlightLabelmap.bind(this) : this._highlightContour.bind(this); - highlightFn( - segmentIndex, - adjustedAlpha, - hideOthers, - segments, - toolGroupId, - animationLength, - segmentationRepresentation - ); + const adjustedAlpha = type === LABELMAP ? alpha : 1 - alpha; + + highlightFn( + segmentIndex, + adjustedAlpha, + hideOthers, + segments, + viewportId, + animationLength, + representation + ); + }); } - public createSegmentationForDisplaySet = async ( - displaySetInstanceUID: string, - options?: { - segmentationId: string; - FrameOfReferenceUID: string; - label: string; + private getAndValidateViewport(viewportId: string) { + const csViewport = + this.servicesManager.services.cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!csViewport) { + throw new Error(`Viewport with id ${viewportId} not found.`); } - ): Promise => { - const { displaySetService } = this.servicesManager.services; - - const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); - - // Todo: we currently only support labelmap for segmentation for a displaySet - const representationType = LABELMAP; - - const volumeId = this._getVolumeIdForDisplaySet(displaySet); - - const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`; - - // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so - // it is easily compressible in worker thread. - await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, { - volumeId: segmentationId, - targetBuffer: { - type: 'Uint8Array', - sharedArrayBuffer: window.SharedArrayBuffer, - }, - }); - - const defaultScheme = this._getDefaultSegmentationScheme(); - - const segmentation: Segmentation = { - ...defaultScheme, - id: segmentationId, - displaySetInstanceUID, - label: options?.label, - // We should set it as active by default, as it created for display - isActive: true, - type: representationType, - FrameOfReferenceUID: - options?.FrameOfReferenceUID || displaySet.instances?.[0]?.FrameOfReferenceUID, - representationData: { - LABELMAP: { - volumeId: segmentationId, - referencedVolumeId: volumeId, // Todo: this is so ugly - }, - }, - description: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`, - }; - - this.addOrUpdateSegmentation(segmentation); - - return segmentationId; - }; + return csViewport; + } /** - * Toggles the visibility of a segmentation in the state, and broadcasts the event. - * Note: this method does not update the segmentation state in the source. It only - * updates the state, and there should be separate listeners for that. - * @param ids segmentation ids + * Sets the visibility of a segmentation representation. + * + * @param viewportId - The ID of the viewport. + * @param segmentationId - The ID of the segmentation. + * @param isVisible - The new visibility state. */ - public toggleSegmentationVisibility = (segmentationId: string): void => { - this._toggleSegmentationVisibility(segmentationId, false); - }; - - public addSegmentationRepresentationToToolGroup = async ( - toolGroupId: string, + private _setSegmentationRepresentationVisibility( + viewportId: string, segmentationId: string, - hydrateSegmentation = false, - representationType = csToolsEnums.SegmentationRepresentations.Labelmap, - suppressEvents = false - ): Promise => { - const segmentation = this.getSegmentation(segmentationId); - - toolGroupId = toolGroupId || this._getApplicableToolGroupId(); - - if (!segmentation) { - throw new Error(`Segmentation with segmentationId ${segmentationId} not found.`); - } - - if (hydrateSegmentation) { - // hydrate the segmentation if it's not hydrated yet - segmentation.hydrated = true; - } - - // Based on the segmentationId, set the colorLUTIndex. - const segmentationRepresentationUIDs = await cstSegmentation.addSegmentationRepresentations( - toolGroupId, - [ - { - segmentationId, - type: representationType, - }, - ] - ); - - // set the latest segmentation representation as active one - this._setActiveSegmentationForToolGroup( + type: SegmentationRepresentations, + isVisible: boolean + ): void { + const representations = this.getSegmentationRepresentations(viewportId, { segmentationId, - toolGroupId, - segmentationRepresentationUIDs[0] - ); - - // add the segmentation segments properly - for (const segment of segmentation.segments) { - if (segment === null || segment === undefined) { - continue; - } - - const { segmentIndex, color, isLocked, isVisible: visibility, opacity } = segment; - - const suppressEvents = true; - - if (color !== undefined) { - this._setSegmentColor(segmentationId, segmentIndex, color, toolGroupId, suppressEvents); - } - - if (opacity !== undefined) { - this._setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId, suppressEvents); - } - - if (visibility !== undefined) { - this._setSegmentVisibility( - segmentationId, - segmentIndex, - visibility, - toolGroupId, - suppressEvents - ); - } - - if (isLocked) { - this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents); - } - } - - if (!suppressEvents) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - }; - - public setSegmentRGBAColor = ( - segmentationId: string, - segmentIndex: number, - rgbaColor, - toolGroupId?: string - ) => { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - this._setSegmentOpacity( - segmentationId, - segmentIndex, - rgbaColor[3], - toolGroupId, // toolGroupId - true - ); - this._setSegmentColor( - segmentationId, - segmentIndex, - [rgbaColor[0], rgbaColor[1], rgbaColor[2]], - toolGroupId, // toolGroupId - true - ); - - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, + type, }); - }; + const representation = representations[0]; - public getToolGroupIdsWithSegmentation = (segmentationId: string): string[] => { - const toolGroupIds = cstSegmentation.state.getToolGroupIdsWithSegmentation(segmentationId); - return toolGroupIds; - }; - - public hydrateSegmentation = (segmentationId: string, suppressEvents = false): void => { - const segmentation = this.getSegmentation(segmentationId); - - if (!segmentation) { - throw new Error(`Segmentation with segmentationId ${segmentationId} not found.`); - } - segmentation.hydrated = true; - - // Not all segmentations have dipslaysets, some of them are derived in the client - this._setDisplaySetIsHydrated(segmentationId, true); - - if (!suppressEvents) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - }; - - private _setDisplaySetIsHydrated(displaySetUID: string, isHydrated: boolean): void { - const { displaySetService } = this.servicesManager.services; - const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); - - if (!displaySet) { + if (!representation) { + console.debug( + 'No segmentation representation found for the given viewportId and segmentationId' + ); return; } - displaySet.isHydrated = isHydrated; - displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false); + cstSegmentation.config.visibility.setSegmentationRepresentationVisibility( + viewportId, + { + segmentationId, + type, + }, + isVisible + ); + } - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation: this.getSegmentation(displaySetUID), + private determineViewportAndSegmentationType(csViewport, segmentation) { + const isVolumeViewport = + csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D; + const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP]; + return { isVolumeViewport, isVolumeSegmentation }; + } + + private async handleViewportConversion( + isVolumeViewport: boolean, + isVolumeSegmentation: boolean, + csViewport: csTypes.IViewport, + segmentation: cstTypes.Segmentation, + viewportId: string, + segmentationId: string, + representationType: csToolsEnums.SegmentationRepresentations + ) { + let representationTypeToUse = representationType; + let isConverted = false; + + const handler = isVolumeViewport ? this.handleVolumeViewportCase : this.handleStackViewportCase; + + ({ representationTypeToUse, isConverted } = await handler.apply(this, [ + csViewport, + segmentation, + isVolumeSegmentation, + viewportId, + segmentationId, + ])); + + return { representationTypeToUse, isConverted }; + } + + private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) { + if (csViewport.type === ViewportType.VOLUME_3D) { + return { representationTypeToUse: SegmentationRepresentations.Surface, isConverted: false }; + } else { + await this.handleVolumeViewport( + csViewport as csTypes.IVolumeViewport, + segmentation, + isVolumeSegmentation + ); + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false }; + } + } + + private async handleStackViewportCase( + csViewport: csTypes.IViewport, + segmentation: cstTypes.Segmentation, + isVolumeSegmentation: boolean, + viewportId: string, + segmentationId: string + ): Promise<{ representationTypeToUse: SegmentationRepresentations; isConverted: boolean }> { + if (isVolumeSegmentation) { + const isConverted = await this.convertStackToVolumeViewport(csViewport); + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted }; + } + + if (updateLabelmapSegmentationImageReferences(viewportId, segmentationId)) { + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false }; + } + + const isConverted = await this.attemptStackToVolumeConversion( + csViewport as csTypes.IStackViewport, + segmentation, + viewportId, + segmentationId + ); + + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted }; + } + + private async _addSegmentationRepresentation( + viewportId: string, + segmentationId: string, + representationType: csToolsEnums.SegmentationRepresentations, + colorLUTIndex: number, + isConverted: boolean + ): Promise { + const representation = { + type: representationType, + segmentationId, + config: { colorLUTOrIndex: colorLUTIndex }, + }; + + const addRepresentation = () => + cstSegmentation.addSegmentationRepresentations(viewportId, [representation]); + + if (isConverted) { + const { viewportGridService } = this.servicesManager.services; + await new Promise(resolve => { + const { unsubscribe } = viewportGridService.subscribe( + viewportGridService.EVENTS.GRID_STATE_CHANGED, + () => { + addRepresentation(); + unsubscribe(); + resolve(); + } + ); + }); + } else { + addRepresentation(); + } + } + private async handleVolumeViewport( + viewport: csTypes.IVolumeViewport, + segmentation: SegmentationData, + isVolumeSegmentation: boolean + ): Promise { + if (isVolumeSegmentation) { + return; // Volume Labelmap on Volume Viewport is natively supported + } + + const frameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const imageIds = getLabelmapImageIds(segmentation.segmentationId); + const segImage = cache.getImage(imageIds[0]); + + if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) { + await convertStackToVolumeLabelmap(segmentation); + } + } + + private async convertStackToVolumeViewport(viewport: csTypes.IViewport): Promise { + const { viewportGridService, cornerstoneViewportService } = this.servicesManager.services; + const state = viewportGridService.getState(); + const gridViewport = state.viewports.get(viewport.id); + + const prevViewPresentation = viewport.getViewPresentation(); + const prevViewReference = viewport.getViewReference(); + const stackViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id); + const { element } = stackViewport; + + const volumeViewportNewVolumeHandler = () => { + const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id); + volumeViewport.setViewPresentation(prevViewPresentation); + volumeViewport.setViewReference(prevViewReference); + volumeViewport.render(); + + element.removeEventListener( + csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + volumeViewportNewVolumeHandler + ); + }; + + element.addEventListener( + csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + volumeViewportNewVolumeHandler + ); + + viewportGridService.setDisplaySetsForViewport({ + viewportId: viewport.id, + displaySetInstanceUIDs: gridViewport.displaySetInstanceUIDs, + viewportOptions: { + ...gridViewport.viewportOptions, + viewportType: ViewportType.ORTHOGRAPHIC, + }, }); + + return true; + } + + private async attemptStackToVolumeConversion( + viewport: csTypes.IStackViewport, + segmentation: SegmentationData, + viewportId: string, + segmentationId: string + ): Promise { + const imageIds = getLabelmapImageIds(segmentation.segmentationId); + const frameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const segImage = cache.getImage(imageIds[0]); + + if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) { + const isConverted = await this.convertStackToVolumeViewport(viewport); + triggerSegmentationRepresentationModified( + viewportId, + segmentationId, + SegmentationRepresentations.Labelmap + ); + + return isConverted; + } + } + + private addSegmentationToSource(segmentationPublicInput: cstTypes.SegmentationPublicInput) { + cstSegmentation.addSegmentations([segmentationPublicInput]); + } + + private updateSegmentationInSource( + segmentationId: string, + payload: Partial + ) { + cstSegmentation.updateSegmentations([{ segmentationId, payload }]); + } + + private _toOHIFSegmentationRepresentation( + viewportId: string, + csRepresentation: cstTypes.SegmentationRepresentation + ): SegmentationRepresentation { + const { segmentationId, type, active, visible } = csRepresentation; + const { colorLUTIndex } = csRepresentation; + + const segmentsRepresentations: { [segmentIndex: number]: SegmentRepresentation } = {}; + + const segmentation = cstSegmentation.state.getSegmentation(segmentationId); + + if (!segmentation) { + throw new Error(`Segmentation with ID ${segmentationId} not found.`); + } + + const segmentIds = Object.keys(segmentation.segments); + + for (const segmentId of segmentIds) { + const segmentIndex = parseInt(segmentId, 10); + + const color = cstSegmentation.config.color.getSegmentIndexColor( + viewportId, + segmentationId, + segmentIndex + ); + + const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility( + viewportId, + { + segmentationId, + type, + }, + segmentIndex + ); + + segmentsRepresentations[segmentIndex] = { + color, + segmentIndex, + opacity: color[3], + visible: isVisible, + }; + } + + const styles = cstSegmentation.config.style.getStyle({ + viewportId, + segmentationId, + type, + }); + + const id = `${segmentationId}-${type}-${viewportId}`; + + return { + id: id, + segmentationId, + label: segmentation.label, + active, + type, + visible, + segments: segmentsRepresentations, + styles, + viewportId, + colorLUTIndex, + config: {}, + }; + } + + private _initSegmentationService() { + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_MODIFIED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REMOVED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, + this._onSegmentationDataModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED, + this._onSegmentationRepresentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, + this._onSegmentationRepresentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, + this._onSegmentationRepresentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_ADDED, + this._onSegmentationAddedFromSource + ); + } + + private getCornerstoneSegmentation(segmentationId: string) { + return cstSegmentation.state.getSegmentation(segmentationId); } private _highlightLabelmap( @@ -1167,31 +1521,32 @@ class SegmentationService extends PubSubService { alpha: number, hideOthers: boolean, segments: Segment[], - toolGroupId: string, + viewportId: string, animationLength: number, - segmentationRepresentation: cstTypes.ToolGroupSpecificRepresentation + representation: cstTypes.SegmentationRepresentation ) { + const { segmentationId } = representation; const newSegmentSpecificConfig = { - [segmentIndex]: { - LABELMAP: { - fillAlpha: alpha, - }, - }, + fillAlpha: alpha, }; if (hideOthers) { + throw new Error('hideOthers is not working right now'); for (let i = 0; i < segments.length; i++) { if (i !== segmentIndex) { newSegmentSpecificConfig[i] = { - LABELMAP: { - fillAlpha: 0, - }, + fillAlpha: 0, }; } } } - const { fillAlpha } = this.getConfiguration(toolGroupId); + const { fillAlpha } = this.getStyle({ + viewportId, + segmentationId, + type: LABELMAP, + segmentIndex, + }) as cstTypes.LabelmapStyle; let startTime: number = null; const animation = (timestamp: number) => { @@ -1202,24 +1557,26 @@ class SegmentationService extends PubSubService { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / animationLength, 1); - cstSegmentation.config.setSegmentSpecificConfig( - toolGroupId, - segmentationRepresentation.segmentationRepresentationUID, + cstSegmentation.config.style.setStyle( { - [segmentIndex]: { - LABELMAP: { - fillAlpha: easeInOutBell(progress, fillAlpha), - }, - }, + segmentationId, + segmentIndex, + type: LABELMAP, + }, + { + fillAlpha: easeInOutBell(progress, fillAlpha), } ); if (progress < 1) { requestAnimationFrame(animation); } else { - cstSegmentation.config.setSegmentSpecificConfig( - toolGroupId, - segmentationRepresentation.segmentationRepresentationUID, + cstSegmentation.config.style.setStyle( + { + segmentationId, + segmentIndex, + type: LABELMAP, + }, {} ); } @@ -1233,34 +1590,48 @@ class SegmentationService extends PubSubService { alpha: number, hideOthers: boolean, segments: Segment[], - toolGroupId: string, + viewportId: string, animationLength: number, - segmentationRepresentation: cstTypes.ToolGroupSpecificRepresentation + representation: cstTypes.SegmentationRepresentation ) { + const { segmentationId } = representation; const startTime = performance.now(); + const prevStyle = cstSegmentation.config.style.getStyle({ + viewportId, + segmentationId, + type: CONTOUR, + segmentIndex, + }) as ContourStyle; + + const prevOutlineWidth = prevStyle.outlineWidth; + // make this configurable + const baseline = Math.max(prevOutlineWidth * 3.5, 5); + const animate = (currentTime: number) => { const progress = (currentTime - startTime) / animationLength; if (progress >= 1) { - cstSegmentation.config.setSegmentSpecificConfig( - toolGroupId, - segmentationRepresentation.segmentationRepresentationUID, + cstSegmentation.config.style.setStyle( + { + segmentationId, + segmentIndex, + type: CONTOUR, + }, {} ); return; } - const reversedProgress = reverseEaseInOutBell(progress, 0.1); - cstSegmentation.config.setSegmentSpecificConfig( - toolGroupId, - segmentationRepresentation.segmentationRepresentationUID, + const reversedProgress = reverseEaseInOutBell(progress, baseline); + + cstSegmentation.config.style.setStyle( { - [segmentIndex]: { - CONTOUR: { - outlineOpacity: reversedProgress, - fillAlpha: reversedProgress, - }, - }, + segmentationId, + segmentIndex, + type: CONTOUR, + }, + { + outlineWidth: reversedProgress, } ); @@ -1270,337 +1641,33 @@ class SegmentationService extends PubSubService { requestAnimationFrame(animate); } - public removeSegmentationRepresentationFromToolGroup( - toolGroupId: string, - segmentationRepresentationUIDsIds?: string[] - ): void { - const uids = segmentationRepresentationUIDsIds || []; - if (!uids.length) { - const representations = cstSegmentation.state.getSegmentationRepresentations(toolGroupId); - - if (!representations || !representations.length) { - return; - } - - uids.push(...representations.map(rep => rep.segmentationRepresentationUID)); - } - - cstSegmentation.removeSegmentationsFromToolGroup(toolGroupId, uids); - } - - /** - * Removes a segmentation and broadcasts the removed event. - * - * @param {string} segmentationId The segmentation id - */ - public remove(segmentationId: string): void { - const segmentation = this.segmentations[segmentationId]; - const wasActive = segmentation.isActive; - - if (!segmentationId || !segmentation) { - console.warn(`No segmentationId provided, or unable to find segmentation by id.`); - return; - } - - const { colorLUTIndex } = segmentation; - const { updatedToolGroupIds } = this._removeSegmentationFromCornerstone(segmentationId); - - // Delete associated colormap - // Todo: bring this back - cstSegmentation.state.removeColorLUT(colorLUTIndex); - - delete this.segmentations[segmentationId]; - - // If this segmentation was active, and there is another segmentation, set another one active. - - if (wasActive) { - const remainingSegmentations = this._getSegmentations(); - - const remainingHydratedSegmentations = remainingSegmentations.filter( - segmentation => segmentation.hydrated - ); - - if (remainingHydratedSegmentations.length) { - const { id } = remainingHydratedSegmentations[0]; - - updatedToolGroupIds.forEach(toolGroupId => { - this._setActiveSegmentationForToolGroup(id, toolGroupId, false); - }); - } - } - - this._setDisplaySetIsHydrated(segmentationId, false); - - this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, { + private _toggleSegmentationRepresentationVisibility = ( + viewportId: string, + segmentationId: string, + type: SegmentationRepresentations + ): void => { + const representations = this.getSegmentationRepresentations(viewportId, { segmentationId, + type, }); - } + const representation = representations[0]; - public getConfiguration = (toolGroupId?: string): SegmentationConfig => { - toolGroupId = toolGroupId ?? this._getApplicableToolGroupId(); - - const brushSize = 1; - // const brushSize = cstUtils.segmentation.getBrushSizeForToolGroup( - // toolGroupId - // ); - - const brushThresholdGate = 1; - // const brushThresholdGate = cstUtils.segmentation.getBrushThresholdForToolGroup( - // toolGroupId - // ); - - const segmentationRepresentations = - this.getSegmentationRepresentationsForToolGroup(toolGroupId); - - const typeToUse = segmentationRepresentations?.[0]?.type || LABELMAP; - - const config = cstSegmentation.config.getGlobalConfig(); - const { renderInactiveSegmentations } = config; - - const representation = config.representations[typeToUse]; - - const { - renderOutline, - outlineWidthActive, - renderFill, - fillAlpha, - fillAlphaInactive, - outlineOpacity, - outlineOpacityInactive, - } = representation; - - return { - brushSize, - brushThresholdGate, - fillAlpha, - fillAlphaInactive, - outlineWidthActive, - renderFill, - renderInactiveSegmentations, - renderOutline, - outlineOpacity, - outlineOpacityInactive, - }; - }; - - public setConfiguration = (configuration: SegmentationConfig): void => { - const { - fillAlpha, - fillAlphaInactive, - outlineWidthActive, - outlineOpacity, - renderFill, - renderInactiveSegmentations, - renderOutline, - } = configuration; - - const setConfigValueIfDefined = (key, value, transformFn = null) => { - if (value !== undefined) { - const transformedValue = transformFn ? transformFn(value) : value; - this._setSegmentationConfig(key, transformedValue); - } - }; - - setConfigValueIfDefined('renderOutline', renderOutline); - setConfigValueIfDefined('outlineWidthActive', outlineWidthActive); - setConfigValueIfDefined('outlineOpacity', outlineOpacity, v => v / 100); - setConfigValueIfDefined('fillAlpha', fillAlpha, v => v / 100); - setConfigValueIfDefined('renderFill', renderFill); - setConfigValueIfDefined('fillAlphaInactive', fillAlphaInactive, v => v / 100); - setConfigValueIfDefined('outlineOpacityInactive', fillAlphaInactive, v => - Math.max(0.75, v / 100) - ); - - if (renderInactiveSegmentations !== undefined) { - const config = cstSegmentation.config.getGlobalConfig(); - config.renderInactiveSegmentations = renderInactiveSegmentations; - cstSegmentation.config.setGlobalConfig(config); - } - - // if (brushSize !== undefined) { - // const { toolGroupService } = this.servicesManager.services; - - // const toolGroupIds = toolGroupService.getToolGroupIds(); - - // toolGroupIds.forEach(toolGroupId => { - // cstUtils.segmentation.setBrushSizeForToolGroup(toolGroupId, brushSize); - // }); - // } - - // if (brushThresholdGate !== undefined) { - // const { toolGroupService } = this.servicesManager.services; - - // const toolGroupIds = toolGroupService.getFirstToolGroupIds(); - - // toolGroupIds.forEach(toolGroupId => { - // cstUtils.segmentation.setBrushThresholdForToolGroup( - // toolGroupId, - // brushThresholdGate - // ); - // }); - // } - - this._broadcastEvent(this.EVENTS.SEGMENTATION_CONFIGURATION_CHANGED, this.getConfiguration()); - }; - - public getLabelmapVolume = (segmentationId: string) => { - return cache.getVolume(segmentationId); - }; - - public getSegmentationRepresentationsForToolGroup = toolGroupId => { - return cstSegmentation.state.getSegmentationRepresentations(toolGroupId); - }; - - public setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) { - this._setSegmentLabel(segmentationId, segmentIndex, label); - } - - private _setSegmentLabel( - segmentationId: string, - segmentIndex: number, - label: string, - suppressEvents = false - ) { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - const segmentInfo = segmentation.segments[segmentIndex]; - - if (segmentInfo === undefined) { - throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`); - } - - segmentInfo.label = label; - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - } - - public shouldRenderSegmentation(viewportDisplaySetInstanceUIDs, segmentationFrameOfReferenceUID) { - if (!viewportDisplaySetInstanceUIDs?.length) { - return false; - } - - const { displaySetService } = this.servicesManager.services; - - let shouldDisplaySeg = false; - - // check if the displaySet is sharing the same frameOfReferenceUID - // with the new segmentation - for (const displaySetInstanceUID of viewportDisplaySetInstanceUIDs) { - const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); - - // Todo: this might not be ideal for use cases such as 4D, since we - // don't want to show the segmentation for all the frames - if ( - displaySet.isReconstructable && - displaySet?.images?.[0]?.FrameOfReferenceUID === segmentationFrameOfReferenceUID - ) { - shouldDisplaySeg = true; - break; - } - } - - return shouldDisplaySeg; - } - - private _getDefaultSegmentationScheme() { - return { - activeSegmentIndex: 1, - cachedStats: {}, - label: '', - segmentsLocked: [], - displayText: [], - hydrated: false, // by default we don't hydrate the segmentation for SEG displaySets - segmentCount: 0, - segments: [], - isVisible: true, - isActive: false, - }; - } - - private _setActiveSegmentationForToolGroup( - segmentationId: string, - toolGroupId: string, - suppressEvents = false - ) { - const segmentations = this._getSegmentations(); - const targetSegmentation = this.getSegmentation(segmentationId); - - if (targetSegmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - segmentations.forEach(segmentation => { - segmentation.isActive = segmentation.id === segmentationId; + const segmentsHidden = cstSegmentation.config.visibility.getHiddenSegmentIndices(viewportId, { + segmentationId, + type: representation.type, }); - const representation = this._getSegmentationRepresentation(segmentationId, toolGroupId); - - cstSegmentation.activeSegmentation.setActiveSegmentationRepresentation( - toolGroupId, - representation.segmentationRepresentationUID + const currentVisibility = segmentsHidden.size === 0; + this._setSegmentationRepresentationVisibility( + viewportId, + segmentationId, + representation.type, + !currentVisibility ); - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation: targetSegmentation, - }); - } - } - - private _toggleSegmentationVisibility = (segmentationId: string, suppressEvents = false) => { - const segmentation = this.segmentations[segmentationId]; - - if (!segmentation) { - throw new Error(`Segmentation with segmentationId ${segmentationId} not found.`); - } - - segmentation.isVisible = !segmentation.isVisible; - - this._updateCornerstoneSegmentationVisibility(segmentationId); - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } }; - private _setActiveSegment(segmentationId: string, segmentIndex: number, suppressEvents = false) { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - + private _setActiveSegment(segmentationId: string, segmentIndex: number) { cstSegmentation.segmentIndex.setActiveSegmentIndex(segmentationId, segmentIndex); - - segmentation.activeSegmentIndex = segmentIndex; - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - } - - private _getSegmentInfo(segmentation: Segmentation, segmentIndex: number) { - const segments = segmentation.segments; - - if (!segments) { - return; - } - - if (segments && segments.length > 0) { - return segments[segmentIndex]; - } } private _getVolumeIdForDisplaySet(displaySet) { @@ -1609,59 +1676,6 @@ class SegmentationService extends PubSubService { return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`; } - private _setSegmentColor = ( - segmentationId: string, - segmentIndex: number, - color: ohifTypes.RGB, - toolGroupId?: string, - suppressEvents = false - ) => { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex); - - if (segmentInfo === undefined) { - throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`); - } - - toolGroupId = toolGroupId ?? this._getApplicableToolGroupId(); - - const segmentationRepresentation = this._getSegmentationRepresentation( - segmentationId, - toolGroupId - ); - - if (!segmentationRepresentation) { - throw new Error('Must add representation to toolgroup before setting segments'); - } - const { segmentationRepresentationUID } = segmentationRepresentation; - - const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex( - toolGroupId, - segmentationRepresentationUID, - segmentIndex - ); - - cstSegmentation.config.color.setColorForSegmentIndex( - toolGroupId, - segmentationRepresentationUID, - segmentIndex, - [...color, rgbaColor[3]] - ); - - segmentInfo.color = color; - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - }; - private _getSegmentCenter(segmentationId, segmentIndex) { const segmentation = this.getSegmentation(segmentationId); @@ -1669,463 +1683,90 @@ class SegmentationService extends PubSubService { return; } - const { cachedStats } = segmentation; + const { segments } = segmentation; + + const { cachedStats } = segments[segmentIndex]; if (!cachedStats) { return; } - const { segmentCenter } = cachedStats; - - if (!segmentCenter) { - return; - } - - const { center } = segmentCenter[segmentIndex]; + const { center } = cachedStats; return center; } - private _setSegmentLocked( - segmentationId: string, - segmentIndex: number, - isLocked: boolean, - suppressEvents = false - ) { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex); - - if (segmentInfo === undefined) { - throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`); - } - - segmentInfo.isLocked = isLocked; - + private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) { cstSegmentation.segmentLocking.setSegmentIndexLocked(segmentationId, segmentIndex, isLocked); - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } } private _setSegmentVisibility( + viewportId: string, segmentationId: string, segmentIndex: number, isVisible: boolean, - toolGroupId?: string, - suppressEvents = false + type?: SegmentationRepresentations ) { - toolGroupId = toolGroupId ?? this._getApplicableToolGroupId(); - - const { segmentationRepresentationUID, segmentation } = this._getSegmentationInfo( - segmentationId, - toolGroupId - ); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex); - - if (segmentInfo === undefined) { - throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`); - } - - segmentInfo.isVisible = isVisible; - - cstSegmentation.config.visibility.setSegmentVisibility( - toolGroupId, - segmentationRepresentationUID, + cstSegmentation.config.visibility.setSegmentIndexVisibility( + viewportId, + { + segmentationId, + type, + }, segmentIndex, isVisible ); - - // make sure to update the isVisible flag on the segmentation - // if a segment becomes invisible then the segmentation should be invisible - // in the status as well, and show correct icon - segmentation.isVisible = segmentation.segments - .filter(Boolean) - .every(segment => segment.isVisible); - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } } - private _setSegmentOpacity = ( - segmentationId: string, - segmentIndex: number, - opacity: number, - toolGroupId?: string, - suppressEvents = false - ) => { - const segmentation = this.getSegmentation(segmentationId); + private _setSegmentLabel(segmentationId: string, segmentIndex: number, segmentLabel: string) { + const segmentation = this.getCornerstoneSegmentation(segmentationId); + const { segments } = segmentation; - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } + segments[segmentIndex].label = segmentLabel; - const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex); - - if (segmentInfo === undefined) { - throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`); - } - - toolGroupId = toolGroupId ?? this._getApplicableToolGroupId(); - - const segmentationRepresentation = this._getSegmentationRepresentation( - segmentationId, - toolGroupId - ); - - if (!segmentationRepresentation) { - throw new Error('Must add representation to toolgroup before setting segments'); - } - const { segmentationRepresentationUID } = segmentationRepresentation; - - const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex( - toolGroupId, - segmentationRepresentationUID, - segmentIndex - ); - - cstSegmentation.config.color.setColorForSegmentIndex( - toolGroupId, - segmentationRepresentationUID, - segmentIndex, - [rgbaColor[0], rgbaColor[1], rgbaColor[2], opacity] - ); - - segmentInfo.opacity = opacity; - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - }; - - private _setSegmentLabel( - segmentationId: string, - segmentIndex: number, - segmentLabel: string, - suppressEvents = false - ) { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - - const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex); - - if (segmentInfo === undefined) { - throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`); - } - - segmentInfo.label = segmentLabel; - - if (suppressEvents === false) { - this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { - segmentation, - }); - } - } - - private _getSegmentationRepresentation(segmentationId, toolGroupId) { - const segmentationRepresentations = - this.getSegmentationRepresentationsForToolGroup(toolGroupId); - - if (!segmentationRepresentations?.length) { - return; - } - - // Todo: this finds the first segmentation representation that matches the segmentationId - // If there are two labelmap representations from the same segmentation, this will not work - const representation = segmentationRepresentations.find( - representation => representation.segmentationId === segmentationId - ); - - return representation; - } - - private _setSegmentationConfig = (property, value) => { - // Todo: currently we only support global config, and we get the type - // from the first segmentation - const typeToUse = this.getSegmentations()[0].type; - - const { cornerstoneViewportService } = this.servicesManager.services; - - const config = cstSegmentation.config.getGlobalConfig(); - - config.representations[typeToUse][property] = value; - - // Todo: add non global (representation specific config as well) - cstSegmentation.config.setGlobalConfig(config); - - const renderingEngine = cornerstoneViewportService.getRenderingEngine(); - const viewportIds = cornerstoneViewportService.getViewportIds(); - - renderingEngine.renderViewports(viewportIds); - }; - - private _initSegmentationService() { - // Connect Segmentation Service to Cornerstone3D. - eventTarget.addEventListener( - csToolsEnums.Events.SEGMENTATION_MODIFIED, - this._onSegmentationModifiedFromSource - ); - - eventTarget.addEventListener( - csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, - this._onSegmentationDataModified - ); - } - - private _onSegmentationDataModified = evt => { - const { segmentationId } = evt.detail; - - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - // Part of add operation, not update operation, exit early. - return; - } - - this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, { - segmentation, - }); - }; - - private _onSegmentationModifiedFromSource = evt => { - const { segmentationId } = evt.detail; - - const segmentation = this.segmentations[segmentationId]; - - if (segmentation === undefined) { - // Part of add operation, not update operation, exit early. - return; - } - - const segmentationState = cstSegmentation.state.getSegmentation(segmentationId); - - if (!segmentationState) { - return; - } - - const { activeSegmentIndex, cachedStats, segmentsLocked, label, type } = segmentationState; - - if (![LABELMAP, CONTOUR].includes(type)) { - throw new Error( - `Unsupported segmentation type: ${type}. Only ${LABELMAP} and ${CONTOUR} are supported.` - ); - } - - const representationData = segmentationState.representationData[type]; - - // TODO: handle other representations when available in cornerstone3D - const segmentationSchema = { - ...segmentation, - activeSegmentIndex, - cachedStats, - displayText: [], - id: segmentationId, - label, - segmentsLocked, - type, - representationData: { - [type]: { - ...representationData, + cstSegmentation.updateSegmentations([ + { + segmentationId, + payload: { + segments, }, }, - }; + ]); + } - try { - this.addOrUpdateSegmentation(segmentationSchema); - } catch (error) { - console.warn(`Failed to add/update segmentation ${segmentationId}`, error); - } - }; - - private _getSegmentationInfo(segmentationId: string, toolGroupId: string) { - const segmentation = this.getSegmentation(segmentationId); - - if (segmentation === undefined) { - throw new Error(`no segmentation for segmentationId: ${segmentationId}`); - } - const segmentationRepresentation = this._getSegmentationRepresentation( + private _onSegmentationDataModifiedFromSource = evt => { + const { segmentationId } = evt.detail; + this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, { segmentationId, - toolGroupId - ); - - if (!segmentationRepresentation) { - throw new Error('Must add representation to toolgroup before setting segments'); - } - - const { segmentationRepresentationUID } = segmentationRepresentation; - - return { segmentationRepresentationUID, segmentation }; - } - - private _removeSegmentationFromCornerstone(segmentationId: string) { - // TODO: This should be from the configuration - const removeFromCache = true; - const segmentationState = cstSegmentation.state; - const sourceSegState = segmentationState.getSegmentation(segmentationId); - const updatedToolGroupIds: Set = new Set(); - - if (!sourceSegState) { - return; - } - - const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(segmentationId); - - toolGroupIds.forEach(toolGroupId => { - const segmentationRepresentations = - segmentationState.getSegmentationRepresentations(toolGroupId); - - const UIDsToRemove = []; - segmentationRepresentations.forEach(representation => { - if (representation.segmentationId === segmentationId) { - UIDsToRemove.push(representation.segmentationRepresentationUID); - updatedToolGroupIds.add(toolGroupId); - } - }); - - // remove segmentation representations - cstSegmentation.removeSegmentationsFromToolGroup( - toolGroupId, - UIDsToRemove, - true // immediate - ); - }); - - // cleanup the segmentation state too - segmentationState.removeSegmentation(segmentationId); - - if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) { - cache.removeVolumeLoadObject(segmentationId); - } - - return { updatedToolGroupIds: Array.from(updatedToolGroupIds) }; - } - - private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) { - if (notYetUpdatedAtSource === false) { - return; - } - const segmentationState = cstSegmentation.state; - const sourceSegmentation = segmentationState.getSegmentation(segmentationId); - const segmentation = this.segmentations[segmentationId]; - const { label, cachedStats } = segmentation; - - // Update the label in the source if necessary - if (sourceSegmentation.label !== label) { - sourceSegmentation.label = label; - } - - if (!isEqual(sourceSegmentation.cachedStats, cachedStats)) { - sourceSegmentation.cachedStats = cachedStats; - } - } - - private _updateCornerstoneSegmentationVisibility = segmentationId => { - const segmentationState = cstSegmentation.state; - const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(segmentationId); - - toolGroupIds.forEach(toolGroupId => { - const segmentationRepresentations = - cstSegmentation.state.getSegmentationRepresentations(toolGroupId); - - if (segmentationRepresentations.length === 0) { - return; - } - - // Todo: this finds the first segmentation representation that matches the segmentationId - // If there are two labelmap representations from the same segmentation, this will not work - const representation = segmentationRepresentations.find( - representation => representation.segmentationId === segmentationId - ); - - const { segmentsHidden } = representation; - - const currentVisibility = segmentsHidden.size === 0 ? true : false; - const newVisibility = !currentVisibility; - - cstSegmentation.config.visibility.setSegmentationVisibility( - toolGroupId, - representation.segmentationRepresentationUID, - newVisibility - ); - - // update segments visibility - const { segmentation } = this._getSegmentationInfo(segmentationId, toolGroupId); - - const segments = segmentation.segments.filter(Boolean); - - segments.forEach(segment => { - segment.isVisible = newVisibility; - }); }); }; - private _getToolGroupIdsWithSegmentation(segmentationId: string) { - const segmentationState = cstSegmentation.state; - const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(segmentationId); - - return toolGroupIds; - } - - private _getFrameOfReferenceUIDForSeg(displaySet) { - const frameOfReferenceUID = displaySet.instance?.FrameOfReferenceUID; - - if (frameOfReferenceUID) { - return frameOfReferenceUID; - } - - // if not found we should try the ReferencedFrameOfReferenceSequence - const referencedFrameOfReferenceSequence = - displaySet.instance?.ReferencedFrameOfReferenceSequence; - - if (referencedFrameOfReferenceSequence) { - return referencedFrameOfReferenceSequence.FrameOfReferenceUID; - } - } - - private _getApplicableToolGroupId = () => { - const { toolGroupService, viewportGridService, cornerstoneViewportService } = - this.servicesManager.services; - - const viewportInfo = cornerstoneViewportService.getViewportInfo( - viewportGridService.getActiveViewportId() - ); - - if (!viewportInfo) { - const toolGroupIds = toolGroupService.getToolGroupIds(); - - return toolGroupIds[0]; - } - - return viewportInfo.getToolGroupId(); + private _onSegmentationRepresentationModifiedFromSource = evt => { + const { segmentationId, viewportId } = evt.detail; + this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, { + segmentationId, + viewportId, + }); }; - /** - * Converts object of objects to array. - * - * @return {Array} Array of objects - */ - private arrayOfObjects = obj => { - return Object.entries(obj).map(e => ({ [e[0]]: e[1] })); + private _onSegmentationModifiedFromSource = ( + evt: cstTypes.EventTypes.SegmentationModifiedEventType + ) => { + const { segmentationId } = evt.detail; + + this._broadcastEvent(this.EVENTS.SEGMENTATION_MODIFIED, { + segmentationId, + }); + }; + + private _onSegmentationAddedFromSource = ( + evt: cstTypes.EventTypes.SegmentationAddedEventType + ) => { + const { segmentationId } = evt.detail; + + this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, { + segmentationId, + }); }; } diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts deleted file mode 100644 index 68c6540cb..000000000 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools'; -import { Types } from '@cornerstonejs/core'; - -type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & { - renderInactiveSegmentations: boolean; - brushSize: number; - brushThresholdGate: number; -}; - -type Segment = { - // the label for the segment - label: string; - // the index of the segment in the segmentation - segmentIndex: number; - // the color of the segment - color: Types.Point3; - // the opacity of the segment - opacity: number; - // whether the segment is visible - isVisible: boolean; - // whether the segment is locked - isLocked: boolean; - // display texts - displayText?: string[]; - // The name of algorithm used to generate the segment. (0062,0009) - algorithmName?: string; - // Type of algorithm used to generate the segment. (0062,0008) - algorithmType?: string; -}; - -type Segmentation = { - // active segment index is the index of the segment that is currently being edited. - activeSegmentIndex: number; - // colorLUTIndex is the index of the color LUT that is currently being used. - colorLUTIndex: number; - // if segmentation contains any data (often calculated from labelmap) - cachedStats: Record; - // displaySetInstanceUID - displaySetInstanceUID: string; - // displayText is the text that is displayed on the segmentation panel (often derived from the data) - displayText?: string[]; - // the id of the segmentation - id: string; - // if the segmentation is the active segmentation being used in the viewer - isActive: boolean; - // if the segmentation is visible in the viewer - isVisible: boolean; - // the frame of reference UID of the segmentation - FrameOfReferenceUID: string; - // the label of the segmentation - label: string; - // the number of segments in the segmentation - segmentCount: number; - // the array of segments with their details, [null, segment1, segment2, ...] - segments: Array; - // the set of segments that are locked - segmentsLocked: Array; - // whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport - // but hydrated SEG -> segmentation that is persisted in the store) - hydrated: boolean; - // the type of the segmentation (e.g., Labelmap etc.) - type: csToolsEnums.SegmentationRepresentations; - // the segmentation representation data - representationData: SegmentationRepresentationData; -}; - -type LabelmapSegmentationData = { - volumeId: string; - referencedVolumeId?: string; -}; - -type SegmentationRepresentationData = { - LABELMAP?: LabelmapSegmentationData; -}; - -export type { SegmentationConfig, Segment, Segmentation }; diff --git a/extensions/cornerstone/src/services/SegmentationService/index.js b/extensions/cornerstone/src/services/SegmentationService/index.ts similarity index 100% rename from extensions/cornerstone/src/services/SegmentationService/index.js rename to extensions/cornerstone/src/services/SegmentationService/index.ts diff --git a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts index 030dd72d8..517231eb9 100644 --- a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts +++ b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts @@ -2,6 +2,7 @@ import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs import { getRenderingEngines, utilities } from '@cornerstonejs/core'; import { pubSubServiceInterface, Types } from '@ohif/core'; +import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer'; const EVENTS = { TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated', @@ -27,6 +28,7 @@ const VOI = 'voi'; const ZOOMPAN = 'zoompan'; const STACKIMAGE = 'stackimage'; const IMAGE_SLICE = 'imageslice'; +const HYDRATE_SEG = 'hydrateseg'; const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup => typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup; @@ -51,6 +53,7 @@ export default class SyncGroupService { // handles both stack and volume viewports [STACKIMAGE]: synchronizers.createImageSliceSynchronizer, [IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer, + [HYDRATE_SEG]: createHydrateSegmentationSynchronizer, }; synchronizersByType: { [key: string]: Synchronizer[] } = {}; @@ -66,18 +69,18 @@ export default class SyncGroupService { private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined { // Initialize if not already done this.synchronizersByType[type] = this.synchronizersByType[type] || []; - const syncCreator = this.synchronizerCreators[type.toLowerCase()]; if (syncCreator) { - const synchronizer = syncCreator(id, options); + // Pass the servicesManager along with other parameters + const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager }); if (synchronizer) { this.synchronizersByType[type].push(synchronizer); return synchronizer; } } else { - console.warn(`Unknown synchronizer type: ${type}, id: ${id}`); + console.debug(`Unknown synchronizer type: ${type}, id: ${id}`); } } diff --git a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts new file mode 100644 index 000000000..561b95d57 --- /dev/null +++ b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts @@ -0,0 +1,79 @@ +import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core'; +import { + SynchronizerManager, + Synchronizer, + Enums, + Types as ToolsTypes, +} from '@cornerstonejs/tools'; + +const { createSynchronizer } = SynchronizerManager; +const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events; + +export default function createHydrateSegmentationSynchronizer( + synchronizerName: string, + { servicesManager, ...options }: { servicesManager: AppTypes.ServicesManager; options } +): Synchronizer { + const stackImageSynchronizer = createSynchronizer( + synchronizerName, + SEGMENTATION_REPRESENTATION_ADDED, + (synchronizerInstance, sourceViewport, targetViewport, sourceEvent) => + segmentationRepresentationModifiedCallback( + synchronizerInstance, + sourceViewport, + targetViewport, + sourceEvent, + { servicesManager, options } + ), + { + eventSource: 'eventTarget', + } + ); + + return stackImageSynchronizer; +} + +const segmentationRepresentationModifiedCallback = async ( + synchronizerInstance: Synchronizer, + sourceViewport: Types.IViewportId, + targetViewport: Types.IViewportId, + sourceEvent: Event, + { servicesManager, options }: { servicesManager: AppTypes.ServicesManager; options: unknown } +) => { + const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType; + + const { segmentationId, viewportId } = event.detail; + const { segmentationService, hangingProtocolService } = servicesManager.services; + + const targetViewportId = targetViewport.viewportId; + + const { viewport } = getEnabledElementByViewportId(targetViewportId); + + const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID(); + + if (!targetFrameOfReferenceUID) { + console.debug('No frame of reference UID found for the target viewport'); + return; + } + + const targetViewportRepresentation = segmentationService.getSegmentationRepresentations( + targetViewportId, + { segmentationId } + ); + + if (targetViewportRepresentation.length > 0) { + return; + } + + // whatever type the source viewport has, we need to add that to the target viewport + const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations( + sourceViewport.viewportId, + { segmentationId } + ); + + const type = sourceViewportRepresentation[0].type; + + await segmentationService.addSegmentationRepresentation(targetViewportId, { + segmentationId, + type, + }); +}; diff --git a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts index baf155764..7c677618b 100644 --- a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts +++ b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts @@ -7,7 +7,7 @@ export type ActionComponentInfo = { id: string; component: ReactNode; location: ViewportActionCornersLocations; - indexPriority: number; + indexPriority?: number; }; class ViewportActionCornersService extends PubSubService { @@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService { public setServiceImplementation({ getState: getStateImplementation, - setComponent: setComponentImplementation, - setComponents: setComponentsImplementation, - clear: clearImplementation, + addComponent: addComponentImplementation, + addComponents: addComponentsImplementation, + clear: clearComponentsImplementation, }): void { if (getStateImplementation) { this.serviceImplementation._getState = getStateImplementation; } - if (setComponentImplementation) { - this.serviceImplementation._setComponent = setComponentImplementation; + if (addComponentImplementation) { + this.serviceImplementation._addComponent = addComponentImplementation; } - if (setComponentsImplementation) { - this.serviceImplementation._setComponents = setComponentsImplementation; + if (addComponentsImplementation) { + this.serviceImplementation._addComponents = addComponentsImplementation; } - if (clearImplementation) { - this.serviceImplementation._clear = clearImplementation; + if (clearComponentsImplementation) { + this.serviceImplementation._clear = clearComponentsImplementation; } } @@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService { return this.serviceImplementation._getState(); } - public setComponent(component: ActionComponentInfo) { - this.serviceImplementation._setComponent(component); + public addComponent(component: ActionComponentInfo) { + this.serviceImplementation._addComponent(component); } - public setComponents(components: Array) { - this.serviceImplementation._setComponents(components); + public addComponents(components: Array) { + this.serviceImplementation._addComponents(components); } public clear(viewportId: string) { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 52607ec79..75fd18a83 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService'; import { RENDERING_ENGINE_ID } from './constants'; import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; -import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation'; +import { + LutPresentation, + PositionPresentation, + Presentations, + SegmentationPresentation, + SegmentationPresentationItem, +} from '../../types/Presentation'; import JumpPresets from '../../utils/JumpPresets'; +import { ViewportProperties } from '@cornerstonejs/core/types'; +import { useLutPresentationStore } from '../../stores/useLutPresentationStore'; +import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore'; +import { useSynchronizersStore } from '../../stores/useSynchronizersStore'; +import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; const EVENTS = { VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged', @@ -176,11 +187,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi * @param presentations - The presentations to apply to the viewport. * @param viewportInfo - Contains a view reference for immediate application */ - public setPresentations( - viewportId: string, - presentations?: Presentations, - viewportInfo?: ViewportInfo - ): void { + public setPresentations(viewportId: string, presentations: Presentations): void { const viewport = this.getCornerstoneViewport(viewportId) as | Types.IStackViewport | Types.IVolumeViewport; @@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return; } - const { lutPresentation, positionPresentation } = presentations; - if (lutPresentation) { - const { presentation } = lutPresentation; - if (viewport instanceof BaseVolumeViewport) { - if (presentation instanceof Map) { - presentation.forEach((properties, volumeId) => { - viewport.setProperties(properties, volumeId); - }); - } else { - viewport.setProperties(presentation); - } - } else { - viewport.setProperties(presentation); - } + const { lutPresentation, positionPresentation, segmentationPresentation } = presentations; + + // Always set the segmentation presentation first, since there might be some + // lutpresentation states that need to be set on the segmentation + // Todo: i think we should even await this + this._setSegmentationPresentation(viewport, segmentationPresentation); + + this._setLutPresentation(viewport, lutPresentation); + this._setPositionPresentation(viewport, { ...positionPresentation, viewportId }); + } + + /** + * Stores the presentation state for a given viewport inside the + * each store. This is used to persist the presentation state + * across different scenarios e.g., when the viewport is changing the + * display set, or when the viewport is moving to a different layout. + * + * @param viewportId The ID of the viewport. + */ + public storePresentation({ viewportId }) { + const presentationIds = this.getPresentationIds(viewportId); + const { syncGroupService } = this.servicesManager.services; + const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId); + + if (!presentationIds || Object.keys(presentationIds).length === 0) { + return null; } - const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference; - if (viewRef) { - viewport.setViewReference(viewRef); + const { lutPresentationId, positionPresentationId, segmentationPresentationId } = + presentationIds; + + const positionPresentation = this._getPositionPresentation(viewportId); + const lutPresentation = this._getLutPresentation(viewportId); + const segmentationPresentation = this._getSegmentationPresentation(viewportId); + + const { setLutPresentation } = useLutPresentationStore.getState(); + const { setPositionPresentation } = usePositionPresentationStore.getState(); + const { setSynchronizers } = useSynchronizersStore.getState(); + const { setSegmentationPresentation } = useSegmentationPresentationStore.getState(); + + if (lutPresentationId) { + setLutPresentation(lutPresentationId, lutPresentation); } - if (positionPresentation?.position) { - viewport.setViewPresentation(positionPresentation.position); + + if (positionPresentationId) { + setPositionPresentation(positionPresentationId, positionPresentation); + } + + if (segmentationPresentationId) { + setSegmentationPresentation(segmentationPresentationId, segmentationPresentation); + } + + if (synchronizers?.length) { + setSynchronizers( + viewportId, + synchronizers.map(synchronizer => ({ + id: synchronizer.id, + sourceViewports: [...synchronizer.getSourceViewports()], + targetViewports: [...synchronizer.getTargetViewports()], + })) + ); } } /** - * Retrieves the position presentation information for a given viewport. - * @param viewportId The ID of the viewport. - * @returns The position presentation object containing various properties - * such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan. + * Retrieves the presentations for a given viewport. + * @param viewportId - The ID of the viewport. + * @returns The presentations for the viewport. */ - public getPositionPresentation(viewportId: string): PositionPresentation { + public getPresentations(viewportId: string): Presentations { + const positionPresentation = this._getPositionPresentation(viewportId); + const lutPresentation = this._getLutPresentation(viewportId); + const segmentationPresentation = this._getSegmentationPresentation(viewportId); + + return { + positionPresentation, + lutPresentation, + segmentationPresentation, + }; + } + + private getPresentationIds(viewportId: string): AppTypes.PresentationIds | null { const viewportInfo = this.viewportsById.get(viewportId); if (!viewportInfo) { - return; + return null; } - const presentationIds = viewportInfo.getPresentationIds(); - - if (!presentationIds) { - return; - } - - const { positionPresentationId } = presentationIds; + return viewportInfo.getPresentationIds(); + } + private _getPositionPresentation(viewportId: string): PositionPresentation { const csViewport = this.getCornerstoneViewport(viewportId); if (!csViewport) { return; } + const viewportInfo = this.viewportsById.get(viewportId); + return { - id: positionPresentationId, viewportType: viewportInfo.getViewportType(), viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(), - position: csViewport.getViewPresentation({ pan: true, zoom: true }), + viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }), + viewportId, }; } - /** - * Retrieves the LUT (Lookup Table) presentation for a given viewport. - * @param viewportId The ID of the viewport. - * @returns The LUT presentation object, or undefined if the viewport does not exist. - */ - public getLutPresentation(viewportId: string): LutPresentation { - const viewportInfo = this.viewportsById.get(viewportId); - if (!viewportInfo) { - return; - } - - const presentationIds = viewportInfo.getPresentationIds(); - - if (!presentationIds) { - return; - } - - const { lutPresentationId } = presentationIds; - + private _getLutPresentation(viewportId: string): LutPresentation { const csViewport = this.getCornerstoneViewport(viewportId) as | Types.IStackViewport | Types.IVolumeViewport; @@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } const cleanProperties = properties => { - if (properties.isComputedVOI) { - delete properties.voiRange; - delete properties.VOILUTFunction; + if (properties?.isComputedVOI) { + delete properties?.voiRange; + delete properties?.VOILUTFunction; } return properties; }; - const presentation = + const properties = csViewport instanceof BaseVolumeViewport ? new Map() : cleanProperties(csViewport.getProperties()); - if (presentation instanceof Map) { - csViewport.getActors().forEach(({ uid: volumeId }) => { - const properties = cleanProperties(csViewport.getProperties(volumeId)); - presentation.set(volumeId, properties); + if (properties instanceof Map) { + const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds(); + volumeIds?.forEach(volumeId => { + const csProps = cleanProperties(csViewport.getProperties(volumeId)); + properties.set(volumeId, csProps); }); } - return { - id: lutPresentationId, - viewportType: viewportInfo.getViewportType(), - presentation, - }; - } - - /** - * Retrieves the presentations for a given viewport. - * @param viewportId - The ID of the viewport. - * @returns The presentations for the viewport. - */ - public getPresentations(viewportId: string): Presentations { const viewportInfo = this.viewportsById.get(viewportId); - if (!viewportInfo) { - return; - } - - const positionPresentation = this.getPositionPresentation(viewportId); - const lutPresentation = this.getLutPresentation(viewportId); return { - positionPresentation, - lutPresentation, + viewportType: viewportInfo.getViewportType(), + properties, }; } - /** - * Stores the presentation state for a given viewport inside the - * stateSyncService. This is used to persist the presentation state - * across different scenarios e.g., when the viewport is changing the - * display set, or when the viewport is moving to a different layout. - * - * @param viewportId The ID of the viewport. - */ - public storePresentation({ viewportId }) { - let presentations = null as Presentations; - try { - presentations = this.getPresentations(viewportId); - if (!presentations?.positionPresentation && !presentations?.lutPresentation) { - return; - } - } catch (error) { - console.warn(error); - return; - } + private _getSegmentationPresentation(viewportId: string): SegmentationPresentation { + const { segmentationService } = this.servicesManager.services; - const { stateSyncService, syncGroupService } = this.servicesManager.services; - - const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId); - - const { positionPresentationStore, synchronizersStore, lutPresentationStore } = - stateSyncService.getState(); - - const { lutPresentation, positionPresentation } = presentations; - const { id: positionPresentationId } = positionPresentation; - const { id: lutPresentationId } = lutPresentation; - - const updateStore = (store, id, value) => ({ ...store, [id]: value }); - - const newState = {} as { [key: string]: any }; - - if (lutPresentationId) { - newState.lutPresentationStore = updateStore( - lutPresentationStore, - lutPresentationId, - lutPresentation - ); - } - - if (positionPresentationId) { - newState.positionPresentationStore = updateStore( - positionPresentationStore, - positionPresentationId, - positionPresentation - ); - } - - if (synchronizers?.length) { - newState.synchronizersStore = updateStore( - synchronizersStore, - viewportId, - synchronizers.map(synchronizer => ({ - id: synchronizer.id, - sourceViewports: [...synchronizer.getSourceViewports()], - targetViewports: [...synchronizer.getTargetViewports()], - })) - ); - } - - stateSyncService.store(newState); + const presentation = segmentationService.getPresentation(viewportId); + return presentation; } /** @@ -423,6 +383,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi // and we would lose the presentation. this.storePresentation({ viewportId: viewportInfo.getViewportId() }); + // Todo: i don't like this here, move it + this.servicesManager.services.segmentationService.clearSegmentationRepresentations( + viewportInfo.getViewportId() + ); + if (!viewportInfo) { throw new Error('element is not enabled for the given viewportId'); } @@ -431,8 +396,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi // since those are the newly set ones, we set them here so that it handles defaults const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions); // Specify an over-ride for the viewport type, even though it is in the public - // viewport options, because the one in the public viewport options is a suggestion - // for initial view, whereas the one in viewportData is a requirement based on the + // viewport options, because the one in the viewportData is a requirement based on the // type of data being displayed. const viewportOptions = viewportInfo.setPublicViewportOptions( publicViewportOptions, @@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi properties.colormap = colormap ?? properties.colormap; } - this._handleOverlays(viewport); + viewport.element.addEventListener(csEnums.Events.VIEWPORT_NEW_IMAGE_SET, evt => { + const { element } = evt.detail; - if (!imageIds?.length) { - return; - } + if (element !== viewport.element) { + return; + } - return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { + csToolsUtils.stackContextPrefetch.enable(element); + }); + + let imageIdsToSet = imageIds; + const res = this._processExtraDisplaySetsForViewport(viewport); + imageIdsToSet = res?.imageIds ?? imageIdsToSet; + + return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); this.setPresentations(viewport.id, presentations, viewportInfo); if (displayArea) { @@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const lastSliceIndex = numberOfSlices - 1; if (imageIndex !== undefined) { - return csToolsUtils.clip(imageIndex, 0, lastSliceIndex); + return csUtils.clip(imageIndex, 0, lastSliceIndex); } if (preset === JumpPresets.First) { @@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const displaySetOptions = displaySetOptionsArray[index]; const { volumeId } = volume; - volumeInputArray.push({ imageIds, volumeId, @@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs); - const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus.loaded); - + const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus?.loaded); if (volumesNotLoaded.length) { if (hangingProtocolService.getShouldPerformCustomImageLoad()) { // delegate the volume loading to the hanging protocol service if it has a custom image load strategy @@ -787,7 +757,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } volumesNotLoaded.forEach(volume => { - if (!volume.loadStatus.loading) { + if (!volume.loadStatus?.loading && volume.load instanceof Function) { volume.load(); } }); @@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } public async setVolumesForViewport(viewport, volumeInputArray, presentations) { - const { displaySetService, toolGroupService, viewportGridService } = - this.servicesManager.services; + const { displaySetService, viewportGridService } = this.servicesManager.services; const viewportInfo = this.getViewportInfo(viewport.id); const displaySetOptions = viewportInfo.getDisplaySetOptions(); @@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const { volumeId } = volumeInput; const displaySetOption = displaySetOptions[index]; const { voi, voiInverted, colormap, displayPreset } = displaySetOption; - const properties = {}; + const properties = {} as ViewportProperties; if (voi && (voi.windowWidth || voi.windowCenter)) { const { lower, upper } = csUtils.windowLevel.toLowHighRange( @@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return { properties, volumeId }; }); + // For SEG and RT viewports + this._processExtraDisplaySetsForViewport(viewport); + await viewport.setVolumes(volumeInputArray); + volumesProperties.forEach(({ properties, volumeId }) => { viewport.setProperties(properties, volumeId); }); this.setPresentations(viewport.id, presentations, viewportInfo); - this._handleOverlays(viewport); - - const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id); - csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id); - const imageIndex = this._getInitialImageIndexForViewport(viewportInfo); if (imageIndex !== undefined) { - csToolsUtils.jumpToSlice(viewport.element, { + csUtils.jumpToSlice(viewport.element, { imageIndex, }); } @@ -863,7 +831,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi }); } - private _handleOverlays(viewport: Types.IStackViewport | Types.IVolumeViewport) { + private _processExtraDisplaySetsForViewport( + viewport: Types.IStackViewport | Types.IVolumeViewport + ) { const { displaySetService } = this.servicesManager.services; // load any secondary displaySets @@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality) ); - if (segOrRTSOverlayDisplaySet) { - this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport); - } else { - // If the displaySet is not a SEG displaySet we assume it is a primary displaySet - // and we can look into hydrated segmentations to check if any of them are - // associated with the primary displaySet - - // get segmentations only returns the hydrated segmentations - this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport); + // if it is only the overlay displaySet, then we need to get the reference + // displaySet imageIds and set them as the imageIds for the viewport, + // here we can do some logic if the reference is missing + // then find the most similar match of displaySet instead + if (!segOrRTSOverlayDisplaySet) { + return; } + + const referenceDisplaySet = displaySetService.getDisplaySetByUID( + segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID + ); + const imageIds = referenceDisplaySet.images.map(image => image.imageId); + this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport); + return { imageIds }; } - private _addSegmentationRepresentationToToolGroupIfNecessary( - displaySetInstanceUIDs: string[], - viewport: any + private addOverlayRepresentationForDisplaySet( + displaySet: OhifTypes.DisplaySet, + viewport: Types.IViewport ) { - const { segmentationService, toolGroupService } = this.servicesManager.services; - - const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id); - - // this only returns hydrated segmentations - const segmentations = segmentationService.getSegmentations(); - - for (const segmentation of segmentations) { - const toolGroupSegmentationRepresentations = - segmentationService.getSegmentationRepresentationsForToolGroup(toolGroup.id) || []; - - // if there is already a segmentation representation for this segmentation - // for this toolGroup, don't bother at all - const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find( - representation => representation.segmentationId === segmentation.id - ); - - if (isSegmentationInToolGroup) { - continue; - } - - // otherwise, check if the hydrated segmentations are in the same FrameOfReferenceUID - // as the primary displaySet, if so add the representation (since it was not there) - const { id: segDisplaySetInstanceUID } = segmentation; - let segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID); - - if (!segFrameOfReferenceUID) { - // if the segmentation displaySet does not have a FrameOfReferenceUID, we might check the - // segmentation itself maybe it has a FrameOfReferenceUID - const { FrameOfReferenceUID } = segmentation; - if (FrameOfReferenceUID) { - segFrameOfReferenceUID = FrameOfReferenceUID; - } - } - - if (!segFrameOfReferenceUID) { - return; - } - - let shouldDisplaySeg = false; - - for (const displaySetInstanceUID of displaySetInstanceUIDs) { - const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(displaySetInstanceUID); - - if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) { - shouldDisplaySeg = true; - break; - } - } - - if (!shouldDisplaySeg) { - return; - } - - segmentationService.addSegmentationRepresentationToToolGroup( - toolGroup.id, - segmentation.id, - false, // already hydrated, - segmentation.type - ); - } - } - - private addOverlayRepresentationForDisplaySet(displaySet: any, viewport: any) { - const { segmentationService, toolGroupService } = this.servicesManager.services; - - const { referencedVolumeId } = displaySet; + const { segmentationService } = this.servicesManager.services; const segmentationId = displaySet.displaySetInstanceUID; - const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id); - const representationType = - referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined + displaySet.Modality === 'SEG' ? csToolsEnums.SegmentationRepresentations.Labelmap : csToolsEnums.SegmentationRepresentations.Contour; - segmentationService.addSegmentationRepresentationToToolGroup( - toolGroup.id, + segmentationService.addSegmentationRepresentation(viewport.id, { segmentationId, - false, - representationType - ); + type: representationType, + }); + + // store the segmentation presentation id in the viewport info + this.storePresentation({ viewportId: viewport.id }); } // Todo: keepCamera is an interim solution until we have a better solution for @@ -1032,7 +939,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } return this._setOtherViewport( - viewport as Types.IViewport, + viewport, viewportData as StackViewportData, viewportInfo, presentations @@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi // calculate the slab thickness based on the volume dimensions const imageVolume = cache.getVolume(volumeId); - const { dimensions } = imageVolume; + const { dimensions, spacing } = imageVolume; const slabThickness = Math.sqrt( - dimensions[0] * dimensions[0] + - dimensions[1] * dimensions[1] + - dimensions[2] * dimensions[2] + Math.pow(dimensions[0] * spacing[0], 2) + + Math.pow(dimensions[1] * spacing[1], 2) + + Math.pow(dimensions[2] * spacing[2], 2) ); return slabThickness; @@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const viewports = this.getRenderingEngine().getViewports(); // Store the current position presentations for each viewport. - viewports.forEach(({ id }) => { - const presentation = this.getPositionPresentation(id); - this.beforeResizePositionPresentations.set(id, presentation); + viewports.forEach(({ id: viewportId }) => { + const presentation = this._getPositionPresentation(viewportId); + this.beforeResizePositionPresentations.set(viewportId, presentation); }); // Resize the rendering engine and render. @@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi this.gridResizeTimeOut = null; }, this.gridResizeDelay); } + + private _setLutPresentation( + viewport: Types.IStackViewport | Types.IVolumeViewport, + lutPresentation: LutPresentation + ): void { + if (!lutPresentation) { + return; + } + + const { properties } = lutPresentation; + if (viewport instanceof BaseVolumeViewport) { + if (properties instanceof Map) { + properties.forEach((propertiesEntry, volumeId) => { + viewport.setProperties(propertiesEntry, volumeId); + }); + } else { + viewport.setProperties(properties); + } + } else { + viewport.setProperties(properties); + } + } + + private _setPositionPresentation( + viewport: Types.IStackViewport | Types.IVolumeViewport, + positionPresentation: PositionPresentation + ): void { + const viewRef = positionPresentation?.viewReference; + if (viewRef) { + viewport.setViewReference(viewRef); + } + + const viewPresentation = positionPresentation?.viewPresentation; + if (viewPresentation) { + viewport.setViewPresentation(viewPresentation); + } + } + + private _setSegmentationPresentation( + viewport: Types.IStackViewport | Types.IVolumeViewport, + segmentationPresentation: SegmentationPresentation + ): void { + if (!segmentationPresentation) { + return; + } + + const { segmentationService } = this.servicesManager.services; + + segmentationPresentation.forEach((presentation: SegmentationPresentationItem) => { + const { segmentationId, type, hydrated } = presentation; + + if (hydrated) { + segmentationService.addSegmentationRepresentation(viewport.id, { + segmentationId, + type, + }); + } + }); + } } export default CornerstoneViewportService; diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index 9281ccf5c..091c7f79d 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -5,7 +5,6 @@ import { VolumeViewport, utilities, } from '@cornerstonejs/core'; -import { Types as CoreTypes } from '@ohif/core'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; @@ -25,7 +24,7 @@ export type ViewportOptions = { toolGroupId: string; viewportId: string; // Presentation ID to store/load presentation state from - presentationIds?: CoreTypes.PresentationIds; + presentationIds?: AppTypes.PresentationIds; orientation?: Enums.OrientationAxis; background?: Types.Point3; displayArea?: Types.DisplayArea; @@ -46,7 +45,7 @@ export type PublicViewportOptions = { id?: string; viewportType?: string; toolGroupId?: string; - presentationIds?: CoreTypes.PresentationIds; + presentationIds?: string[]; viewportId?: string; orientation?: Enums.OrientationAxis; background?: Types.Point3; @@ -283,7 +282,7 @@ class ViewportInfo { return this.viewportOptions; } - public getPresentationIds(): CoreTypes.PresentationIds { + public getPresentationIds(): AppTypes.PresentationIds | null { const { presentationIds } = this.viewportOptions; return presentationIds; } diff --git a/extensions/cornerstone/src/stores/index.ts b/extensions/cornerstone/src/stores/index.ts new file mode 100644 index 000000000..f834c825e --- /dev/null +++ b/extensions/cornerstone/src/stores/index.ts @@ -0,0 +1,4 @@ +export { useLutPresentationStore } from './useLutPresentationStore'; +export { usePositionPresentationStore } from './usePositionPresentationStore'; +export { useSegmentationPresentationStore } from './useSegmentationPresentationStore'; +export { useSynchronizersStore } from './useSynchronizersStore'; diff --git a/extensions/cornerstone/src/stores/presentationUtils.ts b/extensions/cornerstone/src/stores/presentationUtils.ts new file mode 100644 index 000000000..bdc4fdf98 --- /dev/null +++ b/extensions/cornerstone/src/stores/presentationUtils.ts @@ -0,0 +1,42 @@ +const JOIN_STR = '&'; + +// The default lut presentation id if none defined +const DEFAULT_STR = '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: AppTypes.ViewportGrid.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; + } + } +}; + +export { addUniqueIndex, DEFAULT_STR, JOIN_STR }; diff --git a/extensions/cornerstone/src/stores/useLutPresentationStore.ts b/extensions/cornerstone/src/stores/useLutPresentationStore.ts new file mode 100644 index 000000000..0f0e29b0e --- /dev/null +++ b/extensions/cornerstone/src/stores/useLutPresentationStore.ts @@ -0,0 +1,166 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { LutPresentation } from '../types/Presentation'; +import { addUniqueIndex, DEFAULT_STR, JOIN_STR } from './presentationUtils'; + +/** + * Identifier for the LUT Presentation store type. + */ +const PRESENTATION_TYPE_ID = 'lutPresentationId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * Represents the state and actions for managing LUT presentations. + */ +type LutPresentationState = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores LUT presentations indexed by their presentation ID. + */ + lutPresentationStore: Record; + + /** + * Sets the LUT presentation for a given key. + * + * @param key - The key identifying the LUT presentation. + * @param value - The `LutPresentation` to associate with the key. + */ + setLutPresentation: (key: string, value: LutPresentation) => void; + + /** + * Clears all LUT presentations from the store. + */ + clearLutPresentationStore: () => void; + + /** + * Retrieves the presentation ID based on the provided parameters. + * + * @param id - The presentation ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport in grid + * @param options.viewports - All available viewports in grid + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The presentation ID or undefined. + */ + getPresentationId: ( + id: string, + options: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + } + ) => string | undefined; +}; + +/** + * Generates a presentation ID for LUT based on the viewport configuration. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The LUT presentation ID or undefined. + */ +const getLutPresentationId = ( + id: string, + { + viewport, + viewports, + isUpdatingSameViewport, + }: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + } +): string | undefined => { + if (id !== PRESENTATION_TYPE_ID) { + return; + } + + const getLutId = (ds): string => { + if (!ds || !ds.options) { + return DEFAULT_STR; + } + if (ds.options.id) { + return ds.options.id; + } + const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`); + if (!arr.length) { + return DEFAULT_STR; + } + return arr.join(JOIN_STR); + }; + + if (!viewport || !viewport.viewportOptions || !viewport.displaySetInstanceUIDs?.length) { + return; + } + + const { displaySetOptions, displaySetInstanceUIDs } = viewport; + const lutId = getLutId(displaySetOptions[0]); + const lutPresentationArr = [lutId]; + + for (const uid of displaySetInstanceUIDs) { + lutPresentationArr.push(uid); + } + + addUniqueIndex(lutPresentationArr, PRESENTATION_TYPE_ID, viewports, isUpdatingSameViewport); + + return lutPresentationArr.join(JOIN_STR); +}; + +/** + * Creates the LUT Presentation store. + * + * @param set - The zustand set function. + * @returns The LUT Presentation store state and actions. + */ +const createLutPresentationStore = (set): LutPresentationState => ({ + type: PRESENTATION_TYPE_ID, + lutPresentationStore: {}, + + /** + * Sets the LUT presentation for a given key. + */ + setLutPresentation: (key, value) => + set( + state => ({ + lutPresentationStore: { + ...state.lutPresentationStore, + [key]: value, + }, + }), + false, + 'setLutPresentation' + ), + + /** + * Clears all LUT presentations from the store. + */ + clearLutPresentationStore: () => + set({ lutPresentationStore: {} }, false, 'clearLutPresentationStore'), + + /** + * Retrieves the presentation ID based on the provided parameters. + */ + getPresentationId: getLutPresentationId, +}); + +/** + * Zustand store for managing LUT presentations. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useLutPresentationStore = create()( + DEBUG_STORE + ? devtools(createLutPresentationStore, { name: 'LutPresentationStore' }) + : createLutPresentationStore +); diff --git a/extensions/cornerstone/src/stores/usePositionPresentationStore.ts b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts new file mode 100644 index 000000000..1b06bd8e5 --- /dev/null +++ b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts @@ -0,0 +1,172 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { PositionPresentation } from '../types/Presentation'; +import { addUniqueIndex, JOIN_STR } from './presentationUtils'; + +const PRESENTATION_TYPE_ID = 'positionPresentationId'; +const DEBUG_STORE = true; + +/** + * Represents the state and actions for managing position presentations. + */ +type PositionPresentationState = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores position presentations indexed by their presentation ID. + */ + positionPresentationStore: Record; + + /** + * Sets the position presentation for a given key. + * + * @param key - The key identifying the position presentation. + * @param value - The `PositionPresentation` to associate with the key. + */ + setPositionPresentation: (key: string, value: PositionPresentation) => void; + + /** + * Clears all position presentations from the store. + */ + clearPositionPresentationStore: () => void; + + /** + * Retrieves the presentation ID based on the provided parameters. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The position presentation ID or undefined. + */ + getPresentationId: ( + id: string, + options: { + viewport: any; + viewports: any; + isUpdatingSameViewport: boolean; + } + ) => string | undefined; + + getPositionPresentationId: ( + viewport: any, + viewports?: any, + isUpdatingSameViewport?: boolean + ) => string | undefined; +}; + +/** + * Generates a position presentation ID based on the viewport configuration. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The position presentation ID or undefined. + */ +const getPresentationId = ( + id: string, + { + viewport, + viewports, + isUpdatingSameViewport, + }: { + viewport: any; + viewports: any; + isUpdatingSameViewport: boolean; + } +): string | undefined => { + if (id !== PRESENTATION_TYPE_ID) { + return; + } + + if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) { + return; + } + + return getPositionPresentationId(viewport, viewports, isUpdatingSameViewport); +}; + +function getPositionPresentationId(viewport, viewports, isUpdatingSameViewport) { + const { viewportOptions = {}, displaySetInstanceUIDs = [], displaySetOptions = [] } = viewport; + const { id: viewportOptionId, orientation } = viewportOptions; + + const positionPresentationArr = [orientation || 'acquisition']; + if (viewportOptionId) { + positionPresentationArr.push(viewportOptionId); + } + + if (displaySetOptions?.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) { + positionPresentationArr.push(`custom`); + } + + for (const uid of displaySetInstanceUIDs) { + positionPresentationArr.push(uid); + } + + if (viewports && viewports.length && isUpdatingSameViewport !== undefined) { + addUniqueIndex( + positionPresentationArr, + PRESENTATION_TYPE_ID, + viewports, + isUpdatingSameViewport + ); + } else { + positionPresentationArr.push(0); + } + + return positionPresentationArr.join(JOIN_STR); +} + +/** + * Creates the Position Presentation store. + * + * @param set - The zustand set function. + * @returns The Position Presentation store state and actions. + */ +const createPositionPresentationStore = set => ({ + type: PRESENTATION_TYPE_ID, + positionPresentationStore: {}, + + /** + * Sets the position presentation for a given key. + */ + setPositionPresentation: (key, value) => + set( + state => ({ + positionPresentationStore: { + ...state.positionPresentationStore, + [key]: value, + }, + }), + false, + 'setPositionPresentation' + ), + + /** + * Clears all position presentations from the store. + */ + clearPositionPresentationStore: () => + set({ positionPresentationStore: {} }, false, 'clearPositionPresentationStore'), + + /** + * Retrieves the presentation ID based on the provided parameters. + */ + getPresentationId, + getPositionPresentationId: getPositionPresentationId, +}); + +/** + * Zustand store for managing position presentations. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const usePositionPresentationStore = create()( + DEBUG_STORE + ? devtools(createPositionPresentationStore, { name: 'PositionPresentationStore' }) + : createPositionPresentationStore +); diff --git a/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts b/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts new file mode 100644 index 000000000..60cbb8fe0 --- /dev/null +++ b/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts @@ -0,0 +1,238 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { SegmentationPresentation } from '../types/Presentation'; +import { JOIN_STR } from './presentationUtils'; +import { getViewportOrientationFromImageOrientationPatient } from '../utils/getViewportOrientationFromImageOrientationPatient'; + +const PRESENTATION_TYPE_ID = 'segmentationPresentationId'; +const DEBUG_STORE = false; + +/** + * The keys are the presentationId. + */ +type SegmentationPresentationStore = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores segmentation presentations indexed by their presentation ID. + */ + segmentationPresentationStore: Record; + + /** + * Sets the segmentation presentation for a given segmentation ID. + * + * @param presentationId - The presentation ID. + * @param value - The `SegmentationPresentation` to associate with the ID. + */ + setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) => void; + + /** + * Clears all segmentation presentations from the store. + */ + clearSegmentationPresentationStore: () => void; + + /** + * Retrieves the presentation ID based on the provided parameters. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @param options.servicesManager - The services manager instance. + * @returns The segmentation presentation ID or undefined. + */ + getPresentationId: ( + id: string, + options: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + servicesManager: AppTypes.ServicesManager; + } + ) => string | undefined; + + /** + * Adds a new segmentation presentation state. + * + * @param presentationId - The presentation ID. + * @param segmentationPresentation - The `SegmentationPresentation` to add. + * @param servicesManager - The services manager instance. + */ + addSegmentationPresentation: ( + presentationId: string, + segmentationPresentation: SegmentationPresentation, + { servicesManager }: { servicesManager: AppTypes.ServicesManager } + ) => void; + + /** + * Gets the current segmentation presentation ID. + * + * @param params - Parameters for retrieving the segmentation presentation ID. + * @param params.viewport - The current viewport. + * @param params.servicesManager - The services manager instance. + * @returns The current segmentation presentation ID. + */ + getSegmentationPresentationId: ({ + viewport, + servicesManager, + }: { + viewport: AppTypes.ViewportGrid.Viewport; + servicesManager: AppTypes.ServicesManager; + }) => string; +}; + +/** + * Generates a segmentation presentation ID based on the viewport configuration. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @param options.servicesManager - The services manager instance. + * @returns The segmentation presentation ID or undefined. + */ +const getPresentationId = ( + id: string, + { + viewport, + viewports, + isUpdatingSameViewport, + servicesManager, + }: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + servicesManager: AppTypes.ServicesManager; + } +): string | undefined => { + if (id !== PRESENTATION_TYPE_ID) { + return; + } + + return _getSegmentationPresentationId({ viewport, servicesManager }); +}; + +/** + * Helper function to generate the segmentation presentation ID. + * + * @param params - Parameters for generating the segmentation presentation ID. + * @param params.viewport - The current viewport. + * @param params.servicesManager - The services manager instance. + * @returns The segmentation presentation ID or undefined. + */ +const _getSegmentationPresentationId = ({ + viewport, + servicesManager, +}: { + viewport: AppTypes.ViewportGrid.Viewport; + servicesManager: AppTypes.ServicesManager; +}) => { + if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) { + return; + } + + const { displaySetInstanceUIDs, viewportOptions } = viewport; + + let orientation = viewportOptions.orientation; + + if (!orientation) { + // Calculate orientation from the viewport sample image + const displaySet = servicesManager.services.displaySetService.getDisplaySetByUID( + displaySetInstanceUIDs[0] + ); + const sampleImage = displaySet.images?.[0]; + const imageOrientationPatient = sampleImage?.ImageOrientationPatient; + + orientation = getViewportOrientationFromImageOrientationPatient(imageOrientationPatient); + } + + const segmentationPresentationArr = []; + + segmentationPresentationArr.push(...displaySetInstanceUIDs); + + // Uncomment if unique indexing is needed + // addUniqueIndex( + // segmentationPresentationArr, + // 'segmentationPresentationId', + // viewports, + // isUpdatingSameViewport + // ); + + return segmentationPresentationArr.join(JOIN_STR); +}; + +/** + * Creates the Segmentation Presentation store. + * + * @param set - The zustand set function. + * @returns The Segmentation Presentation store state and actions. + */ +const createSegmentationPresentationStore = set => ({ + type: PRESENTATION_TYPE_ID, + segmentationPresentationStore: {}, + + /** + * Clears all segmentation presentations from the store. + */ + clearSegmentationPresentationStore: () => + set({ segmentationPresentationStore: {} }, false, 'clearSegmentationPresentationStore'), + + /** + * Adds a new segmentation presentation to the store. + */ + addSegmentationPresentation: ( + presentationId: string, + segmentationPresentation: SegmentationPresentation, + { servicesManager }: { servicesManager: AppTypes.ServicesManager } + ) => + set( + state => ({ + segmentationPresentationStore: { + ...state.segmentationPresentationStore, + [presentationId]: segmentationPresentation, + }, + }), + false, + 'addSegmentationPresentation' + ), + + /** + * Sets the segmentation presentation for a given presentation ID. + */ + setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) => + set( + state => ({ + segmentationPresentationStore: { + ...state.segmentationPresentationStore, + [presentationId]: value, + }, + }), + false, + 'setSegmentationPresentation' + ), + + /** + * Retrieves the presentation ID based on the provided parameters. + */ + getPresentationId, + + /** + * Retrieves the current segmentation presentation ID. + */ + getSegmentationPresentationId: _getSegmentationPresentationId, +}); + +/** + * Zustand store for managing segmentation presentations. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useSegmentationPresentationStore = create()( + DEBUG_STORE + ? devtools(createSegmentationPresentationStore, { name: 'Segmentation Presentation Store' }) + : createSegmentationPresentationStore +); diff --git a/extensions/cornerstone/src/stores/useSynchronizersStore.ts b/extensions/cornerstone/src/stores/useSynchronizersStore.ts new file mode 100644 index 000000000..612a0c9dc --- /dev/null +++ b/extensions/cornerstone/src/stores/useSynchronizersStore.ts @@ -0,0 +1,84 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +/** + * Identifier for the synchronizers store type. + */ +const PRESENTATION_TYPE_ID = 'synchronizersStoreId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * Information about a single synchronizer. + */ +type SynchronizerInfo = { + id: string; + type: string; + sourceViewports: Array<{ viewportId: string; renderingEngineId: string }>; + targetViewports: Array<{ viewportId: string; renderingEngineId: string }>; +}; + +/** + * State shape for the Synchronizers store. + */ +type SynchronizersState = { + /** + * Stores synchronizer information indexed by a unique key. + */ + synchronizersStore: Record; + + /** + * Sets the synchronizers for a specific viewport. + * + * @param viewportId - The ID of the viewport. + * @param synchronizers - An array of SynchronizerInfo. + */ + setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => void; + + /** + * Clears the entire synchronizers store. + */ + clearSynchronizersStore: () => void; +}; + +/** + * Creates the Synchronizers store. + * + * @param set - The zustand set function. + * @returns The synchronizers store state and actions. + */ +const createSynchronizersStore = (set): SynchronizersState => ({ + synchronizersStore: {}, + type: PRESENTATION_TYPE_ID, + + setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => { + set( + state => ({ + synchronizersStore: { + ...state.synchronizersStore, + [viewportId]: synchronizers, + }, + }), + false, + 'setSynchronizers' + ); + }, + + clearSynchronizersStore: () => { + set({ synchronizersStore: {} }, false, 'clearSynchronizersStore'); + }, +}); + +/** + * Zustand store for managing synchronizers. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useSynchronizersStore = create()( + DEBUG_STORE + ? devtools(createSynchronizersStore, { name: 'SynchronizersStore' }) + : createSynchronizersStore +); diff --git a/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts b/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts index 8f125d1b6..324265660 100644 --- a/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts +++ b/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts @@ -1,5 +1,5 @@ -import { SynchronizerManager, Synchronizer, utilities } from '@cornerstonejs/tools'; -import { EVENTS, getRenderingEngine, Types } from '@cornerstonejs/core'; +import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools'; +import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core'; const frameViewSyncCallback = ( synchronizerInstance: Synchronizer, diff --git a/extensions/cornerstone/src/tools/CalibrationLineTool.ts b/extensions/cornerstone/src/tools/CalibrationLineTool.ts index 6c77fbe79..2c4f46d27 100644 --- a/extensions/cornerstone/src/tools/CalibrationLineTool.ts +++ b/extensions/cornerstone/src/tools/CalibrationLineTool.ts @@ -1,5 +1,5 @@ import { LengthTool, utilities } from '@cornerstonejs/tools'; -import callInputDialog from '../utils/callInputDialog'; +import { callInputDialog } from '@ohif/extension-default'; import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement'; const { calibrateImageSpacing } = utilities; diff --git a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx index c2ba56cbd..f5d036e6f 100644 --- a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx +++ b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx @@ -1,5 +1,5 @@ import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core'; -import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types'; +import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types'; import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools'; import { guid, b64toBlob } from '@ohif/core/src/utils'; import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider'; diff --git a/extensions/cornerstone/src/types/AppTypes.ts b/extensions/cornerstone/src/types/AppTypes.ts index 90997eb27..36cb848b2 100644 --- a/extensions/cornerstone/src/types/AppTypes.ts +++ b/extensions/cornerstone/src/types/AppTypes.ts @@ -9,6 +9,13 @@ import ColorbarServiceType from '../services/ColorbarService'; import * as cornerstone from '@cornerstonejs/core'; import * as cornerstoneTools from '@cornerstonejs/tools'; +import type { + SegmentRepresentation as SegmentRep, + SegmentationData as SegData, + SegmentationRepresentation as SegRep, + SegmentationInfo as SegInfo, +} from '../services/SegmentationService/SegmentationService'; + declare global { namespace AppTypes { export type CornerstoneCacheService = CornerstoneCacheServiceType; @@ -18,6 +25,7 @@ declare global { export type ToolGroupService = ToolGroupServiceType; export type ViewportActionCornersService = ViewportActionCornersServiceType; export type ColorbarService = ColorbarServiceType; + export interface Services { cornerstoneViewportService?: CornerstoneViewportServiceType; toolGroupService?: ToolGroupServiceType; @@ -28,6 +36,19 @@ declare global { colorbarService?: ColorbarServiceType; } + export namespace Segmentation { + export type SegmentRepresentation = SegmentRep; + export type SegmentationData = SegData; + export type SegmentationRepresentation = SegRep; + export type SegmentationInfo = SegInfo; + } + + export interface PresentationIds { + lutPresentationId: string; + positionPresentationId: string; + segmentationPresentationId: string; + } + export interface Test { services?: Services; cornerstone?: typeof cornerstone; diff --git a/extensions/cornerstone/src/types/Presentation.ts b/extensions/cornerstone/src/types/Presentation.ts index 1fd332f82..1d3d0a9da 100644 --- a/extensions/cornerstone/src/types/Presentation.ts +++ b/extensions/cornerstone/src/types/Presentation.ts @@ -1,16 +1,24 @@ import type { Types } from '@cornerstonejs/core'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; /** * Represents a position presentation in a viewport. This is basically * viewport specific camera position and zoom, and not the display set */ export type PositionPresentation = { - id: string; viewportType: string; // The view reference has the basic information as to what image orientation/slice is shown viewReference: Types.ViewReference; // The position information has the zoom/pan and possibly other related information, but not LUT - position: Types.ViewPresentation; + viewPresentation: Types.ViewPresentation; + /** + * Optionals + */ + initialImageIndex?: number; + // viewportId helps when hydrating SR or SEG - we can use it to filter + // presentations to get the one prior to the current viewport and reuse it + // for viewReference and viewPresentation + viewportId?: string; }; /** @@ -20,15 +28,40 @@ export type PositionPresentation = { * itself */ export interface LutPresentation { - id: string; viewportType: string; - presentation: Record | Types.ViewportProperties; + // either a single object with the properties itself or a map of properties with volumeId keys + properties: Record | Types.ViewportProperties; } +/** + * Represents a LUT presentation in a viewport, and is really related + * to displaySets and not the viewport itself. So that is why it can + * be an object with volumeId keys, or a single object with the properties + * itself + * + * each presentation has a segmentationId and a type and a value for + * hydrated and config. + * + * The hydrated property can be a boolean or null. It's null if the segmentation + * representation hasn't been created yet. It's true if the representation is + * currently in the viewport. It's false if the representation was in the viewport + * but has been removed. + * + * Config is the segmentation config, Todo: add stuff here + */ +export type SegmentationPresentationItem = { + segmentationId: string; + type: SegmentationRepresentations; + hydrated: boolean | null; + config?: unknown; +}; + +export type SegmentationPresentation = SegmentationPresentationItem[]; + /** * Presentation can be a PositionPresentation or a LutPresentation. */ -type Presentation = PositionPresentation | LutPresentation; +type Presentation = PositionPresentation | LutPresentation | SegmentationPresentation; /** * Viewport presentations object that can contain a positionPresentation @@ -37,6 +70,7 @@ type Presentation = PositionPresentation | LutPresentation; export type Presentations = { positionPresentation?: PositionPresentation; lutPresentation?: LutPresentation; + segmentationPresentation?: SegmentationPresentation; }; export default Presentation; diff --git a/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx b/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx new file mode 100644 index 000000000..3f1d4699e --- /dev/null +++ b/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState, memo } from 'react'; + +const MODALITIES_REQUIRING_CINE_AUTO_MOUNT = ['OT', 'US']; + +const ActiveViewportBehavior = memo( + ({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => { + const { displaySetService, cineService, viewportGridService, customizationService } = + servicesManager.services; + + const [activeViewportId, setActiveViewportId] = useState(viewportId); + + useEffect(() => { + const subscription = viewportGridService.subscribe( + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + ({ viewportId }) => setActiveViewportId(viewportId) + ); + + return () => subscription.unsubscribe(); + }, [viewportId, viewportGridService]); + + useEffect(() => { + if (cineService.isViewportCineClosed(activeViewportId)) { + return; + } + + const displaySetInstanceUIDs = + viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId); + + if (!displaySetInstanceUIDs) { + return; + } + + const displaySets = displaySetInstanceUIDs.map(uid => + displaySetService.getDisplaySetByUID(uid) + ); + + if (!displaySets.length) { + return; + } + + const modalities = displaySets.map(displaySet => displaySet?.Modality); + + const { modalities: sourceModalities } = customizationService.getModeCustomization( + 'autoCineModalities', + { + id: 'autoCineModalities', + modalities: MODALITIES_REQUIRING_CINE_AUTO_MOUNT, + } + ); + + const requiresCine = modalities.some(modality => sourceModalities.includes(modality)); + + if (requiresCine && !cineService.getState().isCineEnabled) { + cineService.setIsCineEnabled(true); + } + }, [ + activeViewportId, + cineService, + viewportGridService, + displaySetService, + customizationService, + ]); + + return null; + }, + arePropsEqual +); + +ActiveViewportBehavior.displayName = 'ActiveViewportBehavior'; + +function arePropsEqual(prevProps, nextProps) { + return ( + prevProps.viewportId === nextProps.viewportId && + prevProps.servicesManager === nextProps.servicesManager + ); +} + +export default ActiveViewportBehavior; diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx index 0d512dfe8..5e72d37ab 100644 --- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx +++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx @@ -8,7 +8,6 @@ import { BaseVolumeViewport, } from '@cornerstonejs/core'; import { ToolGroupManager } from '@cornerstonejs/tools'; -import PropTypes from 'prop-types'; import { ViewportDownloadForm } from '@ohif/ui'; import { getEnabledElement as OHIFgetEnabledElement } from '../state'; @@ -22,7 +21,7 @@ const CornerstoneViewportDownloadForm = ({ onClose, activeViewportId: activeViewportIdProp, cornerstoneViewportService, -}) => { +}: withAppTypes) => { const enabledElement = OHIFgetEnabledElement(activeViewportIdProp); const activeViewportElement = enabledElement?.element; const activeViewportEnabledElement = getEnabledElement(activeViewportElement); @@ -246,9 +245,4 @@ const CornerstoneViewportDownloadForm = ({ ); }; -CornerstoneViewportDownloadForm.propTypes = { - onClose: PropTypes.func, - activeViewportId: PropTypes.string.isRequired, -}; - export default CornerstoneViewportDownloadForm; diff --git a/extensions/cornerstone/src/utils/dicomLoaderService.js b/extensions/cornerstone/src/utils/dicomLoaderService.js index b9d5ea6dd..ba43213fd 100644 --- a/extensions/cornerstone/src/utils/dicomLoaderService.js +++ b/extensions/cornerstone/src/utils/dicomLoaderService.js @@ -161,7 +161,7 @@ class DicomLoaderService { getDicomDataMethod = fetchIt.bind(this, imageId); break; default: - throw new Error(`Unsupported image type: ${loaderType} for imageId: ${imageId}`); + return; } return getDicomDataMethod(); diff --git a/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts b/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts index e63e85f79..c8f4cf300 100644 --- a/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts +++ b/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts @@ -1,5 +1,5 @@ import { getEnabledElement } from '@cornerstonejs/core'; -import { IEnabledElement } from '@cornerstonejs/core/dist/esm/types'; +import { IEnabledElement } from '@cornerstonejs/core/types'; import { getEnabledElement as OHIFgetEnabledElement } from '../state'; diff --git a/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts b/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts index 2d44dd983..d70410188 100644 --- a/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts +++ b/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts @@ -22,7 +22,7 @@ export default function getCornerstoneViewportType( return Enums.ViewportType.VIDEO; } if (lowerViewportType === WHOLESLIDE) { - return Enums.ViewportType.WholeSlide; + return Enums.ViewportType.WHOLE_SLIDE; } if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) { diff --git a/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts b/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts new file mode 100644 index 000000000..583b972e6 --- /dev/null +++ b/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts @@ -0,0 +1,56 @@ +import { CONSTANTS, utilities } from '@cornerstonejs/core'; + +const { MPR_CAMERA_VALUES } = CONSTANTS; + +/** + * Determines the viewport orientation (axial, sagittal, or coronal) based on the image orientation patient values. + * This is done by comparing the view vectors with predefined MPR camera values. + * + * @param imageOrientationPatient - Array of 6 numbers representing the image orientation patient values. + * The first 3 numbers represent the direction cosines of the first row and the second 3 numbers + * represent the direction cosines of the first column. + * + * @returns The viewport orientation as a string ('axial', 'sagittal', 'coronal') or undefined if + * the orientation cannot be determined or if the input is invalid. + * + * @example + * ```typescript + * const orientation = getViewportOrientationFromImageOrientationPatient([1,0,0,0,1,0]); + * console.debug(orientation); // 'axial' + * ``` + */ +export const getViewportOrientationFromImageOrientationPatient = ( + imageOrientationPatient: number[] +): string | undefined => { + if (!imageOrientationPatient || imageOrientationPatient.length !== 6) { + return undefined; + } + + const viewRight = imageOrientationPatient.slice(0, 3); + const viewDown = imageOrientationPatient.slice(3, 6); + const viewUp = [-viewDown[0], -viewDown[1], -viewDown[2]]; + + // Compare vectors with MPR camera values using utilities.isEqual + if ( + utilities.isEqual(viewRight, MPR_CAMERA_VALUES.axial.viewRight) && + utilities.isEqual(viewUp, MPR_CAMERA_VALUES.axial.viewUp) + ) { + return 'axial'; + } + + if ( + utilities.isEqual(viewRight, MPR_CAMERA_VALUES.sagittal.viewRight) && + utilities.isEqual(viewUp, MPR_CAMERA_VALUES.sagittal.viewUp) + ) { + return 'sagittal'; + } + + if ( + utilities.isEqual(viewRight, MPR_CAMERA_VALUES.coronal.viewRight) && + utilities.isEqual(viewUp, MPR_CAMERA_VALUES.coronal.viewUp) + ) { + return 'coronal'; + } + + return undefined; +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts index 022b70cab..a21756c4e 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts @@ -1,5 +1,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; @@ -19,9 +21,11 @@ const Angle = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -55,7 +59,7 @@ const Angle = { const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); - const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); @@ -65,6 +69,8 @@ const Angle = { FrameOfReferenceUID, points, textBox, + isLocked, + isVisible, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, @@ -158,12 +164,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio }; } -function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } // Area is the same for all series const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; @@ -181,9 +190,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { return displayText; } const roundedAngle = utils.roundNumber(angle, 2); - displayText.push( - `${roundedAngle} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})` - ); + + displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts index c91239144..bffba4a2a 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts @@ -1,6 +1,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getIsLocked } from './utils/getIsLocked'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; - +import { getIsVisible } from './utils/getIsVisible'; const Length = { toAnnotation: measurement => {}, @@ -17,9 +18,11 @@ const Length = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -53,7 +56,7 @@ const Length = { const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); - const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const displayText = getDisplayText(mappedAnnotations, displaySet); return { uid: annotationUID, @@ -61,6 +64,8 @@ const Length = { FrameOfReferenceUID, points, textBox, + isLocked, + isVisible, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, @@ -107,15 +112,17 @@ function getMappedAnnotations(annotation, displaySetService) { return annotations; } -function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations) { - return ''; +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; } - const displayText = []; - - // Area is the same for all series - const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const { SeriesNumber, SOPInstanceUID, frameNumber, text } = mappedAnnotations[0]; const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -127,7 +134,13 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`); + // Add the annotation text to the primary array + if (text) { + displayText.primary.push(text); + } + + // Add the series information to the secondary array + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts index fceed7e48..241848b43 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts @@ -1,9 +1,9 @@ -import { annotation } from '@cornerstonejs/tools'; - import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; const Bidirectional = { toAnnotation: measurement => {}, @@ -14,9 +14,12 @@ const Bidirectional = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -50,7 +53,7 @@ const Bidirectional = { const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); - const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); @@ -60,6 +63,8 @@ const Bidirectional = { FrameOfReferenceUID, points, textBox, + isLocked, + isVisible, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, @@ -153,12 +158,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio }; } -function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } // Area is the same for all series const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; @@ -175,10 +183,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - displayText.push( - `L: ${roundedLength} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})` - ); - displayText.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`); + displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`); + displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts index d6dba8762..ff529602f 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts @@ -3,6 +3,8 @@ import { getDisplayUnit } from './utils'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; import { getStatisticDisplayString } from './utils/getValueDisplayString'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; const CircleROI = { toAnnotation: measurement => {}, @@ -13,9 +15,12 @@ const CircleROI = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -49,7 +54,7 @@ const CircleROI = { const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); - const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); @@ -59,6 +64,8 @@ const CircleROI = { FrameOfReferenceUID, points, textBox, + isLocked, + isVisible, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, @@ -161,12 +168,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio }; } -function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } // Area is the same for all series const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; @@ -183,7 +193,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. const roundedArea = utils.roundNumber(area || 0, 2); - displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { @@ -191,10 +201,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const maxStr = getStatisticDisplayString(max, unit, 'max'); - const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`; - if (!displayText.includes(str)) { - displayText.push(str); - } + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); }); return displayText; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts index 717fcaf0a..e4f3449d8 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts @@ -1,5 +1,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; @@ -19,9 +21,12 @@ const CobbAngle = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { console.warn('Cobb Angle tool: Missing metadata or data'); return null; @@ -55,7 +60,7 @@ const CobbAngle = { const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); - const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); @@ -65,6 +70,8 @@ const CobbAngle = { FrameOfReferenceUID, points, textBox, + isLocked, + isVisible, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, @@ -158,14 +165,17 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio }; } -function getDisplayText(mappedAnnotations, displaySet, customizationService) { +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; + return displayText; } - const displayText = []; - - // Area is the same for all series + // Angle is the same for all series const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -181,9 +191,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { return displayText; } const roundedAngle = utils.roundNumber(angle, 2); - displayText.push( - `${roundedAngle} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})` - ); + + displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts index d9ccd35e5..7cdbfceb1 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts @@ -1,5 +1,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; import { getStatisticDisplayString } from './utils/getValueDisplayString'; @@ -13,9 +15,12 @@ const EllipticalROI = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -60,6 +65,8 @@ const EllipticalROI = { points, textBox, metadata, + isLocked, + isVisible, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, referencedImageId, @@ -162,11 +169,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio } function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } // Area is the same for all series const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; @@ -182,17 +192,15 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; const roundedArea = utils.roundNumber(area, 2); - displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { const { unit, max, SeriesNumber } = mappedAnnotation; const maxStr = getStatisticDisplayString(max, unit, 'max'); - const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`; - if (!displayText.includes(str)) { - displayText.push(str); - } + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); }); return displayText; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts index 71abc6148..0b0c1c6e8 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts @@ -1,6 +1,9 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; +import { config } from '@cornerstonejs/tools/annotation'; const Length = { toAnnotation: measurement => {}, @@ -18,9 +21,17 @@ const Length = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + const colorString = config.style.getStyleProperty('color', { annotationUID }); + + // color string is like 'rgb(255, 255, 255)' we need them to be in RGBA array [255, 255, 255, 255] + // Todo: this should be in a utility + // const color = colorString.replace('rgb(', '').replace(')', '').split(',').map(Number); + if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -54,7 +65,7 @@ const Length = { const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); - const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); @@ -64,7 +75,10 @@ const Length = { FrameOfReferenceUID, points, textBox, + isLocked, + isVisible, metadata, + // color, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, referencedImageId, @@ -158,14 +172,17 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio }; } -function getDisplayText(mappedAnnotations, displaySet, customizationService) { +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; + return displayText; } - const displayText = []; - - // Area is the same for all series + // Length is the same for all series const { length, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0]; const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -182,7 +199,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { return displayText; } const roundedLength = utils.roundNumber(length, 2); - displayText.push(`${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`); + displayText.primary.push(`${roundedLength} ${unit}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts index ff6809a7a..a9a3d03b3 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts @@ -2,7 +2,8 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { getDisplayUnit } from './utils'; import { utils } from '@ohif/core'; - +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; /** * Represents a mapping utility for Livewire measurements. */ @@ -28,6 +29,8 @@ const LivewireContour = { const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('Livewire tool: Missing metadata or data'); return null; @@ -65,7 +68,9 @@ const LivewireContour = { toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, label: data.label, - displayText: getDisplayText(annotation, displaySet, customizationService), + isLocked, + isVisible, + displayText: getDisplayText(annotation, displaySet), data: data.cachedStats, type: getValueTypeFromToolType(toolName), getReport: () => getColumnValueReport(annotation, customizationService), @@ -119,7 +124,7 @@ function getColumnValueReport(annotation, customizationService) { * @param {Object} displaySet - The display set object. * @returns {string[]} - An array of display text. */ -function getDisplayText(annotation, displaySet, customizationService) { +function getDisplayText(annotation, displaySet) { const { metadata, data } = annotation; if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) { @@ -142,19 +147,26 @@ function getDisplayText(annotation, displaySet, customizationService) { const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; const { SeriesNumber } = displaySet; + let seriesText = null; if (SeriesNumber !== undefined) { - displayText.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`; } + const texts = []; if (area) { - /** - * Add Area - * Area sometimes becomes undefined if `preventHandleOutsideImage` is off - */ const roundedArea = utils.roundNumber(area || 0, 2); - displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + texts.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); } + if (seriesText) { + texts.push(seriesText); + } + + displayText.push({ + text: texts, + series: seriesText, + }); + return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts index 252e72792..b3309e723 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts @@ -1,7 +1,10 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; - +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import { getDisplayUnit } from './utils'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; /** * Represents a mapping utility for Planar Freehand ROI measurements. */ @@ -12,10 +15,11 @@ const PlanarFreehandROI = { * Maps cornerstone annotation event data to measurement service format. * * @param {Object} csToolsEventDetail Cornerstone event data - * @param {DisplaySetService} DisplaySetService Service for managing display sets + * @param {DisplaySetService} displaySetService Service for managing display sets * @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports * @param {Function} getValueTypeFromToolType Function to get value type from tool type - * @returns {Measurement} Measurement instance + * @param {CustomizationService} customizationService Service for customization + * @returns {Measurement | null} Measurement instance or null if invalid */ toMeasurement: ( csToolsEventDetail, @@ -27,8 +31,10 @@ const PlanarFreehandROI = { const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { - console.warn('PlanarFreehandROI tool: Missing metadata or data'); + console.debug('PlanarFreehandROI tool: Missing metadata or data'); return null; } @@ -51,6 +57,9 @@ const PlanarFreehandROI = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; } + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + const displayText = getDisplayText(mappedAnnotations, displaySet); + return { uid: annotationUID, SOPInstanceUID, @@ -65,21 +74,72 @@ const PlanarFreehandROI = { toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, label: data.label, - displayText: getDisplayText(annotation, displaySet, customizationService, displaySetService), + displayText: displayText, data: data.cachedStats, type: getValueTypeFromToolType(toolName), getReport: () => getColumnValueReport(annotation, customizationService), + isLocked, + isVisible, }; }, }; /** - * This function is used to convert the measurement data to a - * format that is suitable for report generation (e.g. for the csv report). - * The report returns a list of columns and corresponding values. + * Maps annotations to a structured format with relevant attributes. * - * @param {object} annotation - * @returns {object} Report's content from this tool + * @param {Object} annotation The annotation object. + * @param {DisplaySetService} displaySetService Service for managing display sets. + * @returns {Array} Mapped annotations. + */ +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + mean, + stdDev, + max, + area, + areaUnit, + }); + }); + + return annotations; +} + +/** + * Converts the measurement data to a format suitable for report generation. + * + * @param {object} annotation The annotation object. + * @param {CustomizationService} customizationService Service for customization. + * @returns {object} Report's content. */ function getColumnValueReport(annotation, customizationService) { const { PlanarFreehandROI } = customizationService.get('cornerstone.measurements'); @@ -118,28 +178,25 @@ function getColumnValueReport(annotation, customizationService) { /** * Retrieves the display text for an annotation in a display set. * - * @param {Object} annotation - The annotation object. - * @param {Object} displaySet - The display set object. - * @returns {string[]} - An array of display text. + * @param {Array} mappedAnnotations The mapped annotations. + * @param {Object} displaySet The display set object. + * @returns {Object} Display text with primary and secondary information. */ -function getDisplayText(annotation, displaySet, customizationService, displaySetService) { - const { PlanarFreehandROI } = customizationService.get('cornerstone.measurements'); - const { displayText: displayTextClosed, displayTextOpen } = PlanarFreehandROI; +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; - const { metadata, data } = annotation; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } - const isClosed = data.contour?.closed; - const displayText = isClosed ? displayTextClosed : displayTextOpen; - - const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes( - metadata.referencedImageId, - displaySetService, - annotation - ); - - const displayTextArray = []; + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + let InstanceNumber; if (instance) { InstanceNumber = instance.InstanceNumber; @@ -148,47 +205,19 @@ function getDisplayText(annotation, displaySet, customizationService, displaySet const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - const { SeriesNumber } = displaySet; - if (SeriesNumber !== undefined) { - displayTextArray.push(`S: ${SeriesNumber}${instanceText}${frameText}`); - } + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); - const stats = - data.cachedStats[`imageId:${metadata.referencedImageId}`] || - Array.from(Object.values(data.cachedStats))[0]; + mappedAnnotations.forEach(mappedAnnotation => { + const { unit, max, SeriesNumber } = mappedAnnotation; - if (!stats) { - return displayTextArray; - } + const maxStr = getStatisticDisplayString(max, unit, 'max'); - const roundValues = values => { - if (Array.isArray(values)) { - return values.map(value => { - if (isNaN(value)) { - return value; - } - return utils.roundNumber(value, 2); - }); - } - return isNaN(values) ? [values] : [utils.roundNumber(values, 2)]; - }; - - const findUnitForValue = (displayTextItems, value) => - displayTextItems.find(({ type, for: filter }) => type === 'unit' && filter.includes(value)) - ?.value; - - const formatDisplayText = (displayName, result, unit) => - `${displayName}: ${roundValues(result).join(', ')} ${unit}`; - - displayText.forEach(({ displayName, value, type }) => { - if (type === 'value') { - const result = stats[value]; - const unit = stats[findUnitForValue(displayText, value)] || ''; - displayTextArray.push(formatDisplayText(displayName, result, unit)); - } + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); }); - return displayTextArray; + return displayText; } export default PlanarFreehandROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts index e6b401aff..8ad3e70e6 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts @@ -2,7 +2,8 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import { getDisplayUnit } from './utils'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; - +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; const Probe = { toAnnotation: measurement => {}, @@ -21,7 +22,8 @@ const Probe = { ) => { const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; - + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('Probe tool: Missing metadata or data'); return null; @@ -65,6 +67,8 @@ const Probe = { FrameOfReferenceUID, points, metadata, + isLocked, + isVisible, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, referencedImageId, @@ -155,11 +159,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio } function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } const { value, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; @@ -172,13 +179,12 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - if (value === undefined) { - return displayText; + + if (value !== undefined) { + const roundedValue = utils.roundNumber(value, 2); + displayText.primary.push(`${roundedValue} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); } - const roundedValue = utils.roundNumber(value, 2); - displayText.push( - `${roundedValue} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})` - ); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts index ade974f91..c35636b10 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -3,6 +3,8 @@ import { getDisplayUnit } from './utils'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; import { getStatisticDisplayString } from './utils/getValueDisplayString'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; const RectangleROI = { toAnnotation: measurement => {}, @@ -13,8 +15,10 @@ const RectangleROI = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('Rectangle ROI tool: Missing metadata or data'); @@ -71,6 +75,8 @@ const RectangleROI = { data: data.cachedStats, type: getValueTypeFromToolType(toolName), getReport, + isLocked, + isVisible, }; }, }; @@ -163,11 +169,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio } function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } // Area is the same for all series const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; @@ -184,7 +193,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. const roundedArea = utils.roundNumber(area || 0, 2); - displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { @@ -192,10 +201,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const maxStr = getStatisticDisplayString(max, unit, 'max'); - const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`; - if (!displayText.includes(str)) { - displayText.push(str); - } + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); }); return displayText; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts index ca01ce456..8ced413e8 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts @@ -1,25 +1,32 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import { getDisplayUnit } from './utils'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; /** * Represents a mapping utility for Spline ROI measurements. */ const SplineROI = { - toAnnotation: measurement => {}, + toAnnotation: measurement => { + // Implementation for converting measurement to annotation + }, /** * Maps cornerstone annotation event data to measurement service format. * - * @param {Object} csToolsEventDetail Cornerstone event data - * @param {DisplaySetService} DisplaySetService Service for managing display sets - * @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports - * @param {Function} getValueTypeFromToolType Function to get value type from tool type - * @returns {Measurement} Measurement instance + * @param {Object} csToolsEventDetail - Cornerstone event data + * @param {DisplaySetService} displaySetService - Service for managing display sets + * @param {CornerstoneViewportService} CornerstoneViewportService - Service for managing viewports + * @param {Function} getValueTypeFromToolType - Function to get value type from tool type + * @param {CustomizationService} customizationService - Service for customization + * @returns {Measurement | null} Measurement instance or null if invalid */ toMeasurement: ( csToolsEventDetail, - DisplaySetService, + displaySetService, CornerstoneViewportService, getValueTypeFromToolType, customizationService @@ -27,6 +34,8 @@ const SplineROI = { const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('SplineROI tool: Missing metadata or data'); return null; @@ -39,18 +48,21 @@ const SplineROI = { } const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } = - getSOPInstanceAttributes(referencedImageId); + getSOPInstanceAttributes(referencedImageId, displaySetService, annotation); let displaySet; if (SOPInstanceUID) { - displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, SeriesInstanceUID ); } else { - displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; } + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + const displayText = getDisplayText(mappedAnnotations, displaySet); + return { uid: annotationUID, SOPInstanceUID, @@ -61,24 +73,76 @@ const SplineROI = { frameNumber, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, + referencedImageId, toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, label: data.label, - displayText: getDisplayText(annotation, displaySet, customizationService), + displayText: displayText, data: data.cachedStats, type: getValueTypeFromToolType(toolName), getReport: () => getColumnValueReport(annotation, customizationService), + isLocked, + isVisible, }; }, }; /** - * This function is used to convert the measurement data to a - * format that is suitable for report generation (e.g. for the csv report). - * The report returns a list of columns and corresponding values. + * Maps annotations to a structured format with relevant attributes. * - * @param {object} annotation - * @returns {object} Report's content from this tool + * @param {Object} annotation - The annotation object. + * @param {DisplaySetService} displaySetService - Service for managing display sets. + * @returns {Array} Mapped annotations. + */ +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + mean, + stdDev, + max, + area, + areaUnit, + }); + }); + + return annotations; +} + +/** + * Converts the measurement data to a format suitable for report generation. + * + * @param {object} annotation - The annotation object. + * @param {CustomizationService} customizationService - Service for customization. + * @returns {object} Report's content. */ function getColumnValueReport(annotation, customizationService) { const { SplineROI } = customizationService.get('cornerstone.measurements'); @@ -107,11 +171,6 @@ function getColumnValueReport(annotation, customizationService) { /** Add points */ if (data.contour.polyline) { - /** - * Points has the form of [[x1, y1, z1], [x2, y2, z2], ...] - * convert it to string of [[x1 y1 z1];[x2 y2 z2];...] - * so that it can be used in the CSV report - */ columns.push('points'); values.push(data.contour.polyline.map(p => p.join(' ')).join(';')); } @@ -122,23 +181,25 @@ function getColumnValueReport(annotation, customizationService) { /** * Retrieves the display text for an annotation in a display set. * - * @param {Object} annotation - The annotation object. + * @param {Array} mappedAnnotations - The mapped annotations. * @param {Object} displaySet - The display set object. - * @returns {string[]} - An array of display text. + * @returns {Object} Display text with primary and secondary information. */ -function getDisplayText(annotation, displaySet, customizationService) { - const { SplineROI } = customizationService.get('cornerstone.measurements'); - const { displayText } = SplineROI; - const { metadata, data } = annotation; +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; - if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) { - return []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; } - const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId); - const displayTextArray = []; + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + let InstanceNumber; if (instance) { InstanceNumber = instance.InstanceNumber; @@ -147,41 +208,20 @@ function getDisplayText(annotation, displaySet, customizationService) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - const { SeriesNumber } = displaySet; - if (SeriesNumber !== undefined) { - displayTextArray.push(`S: ${SeriesNumber}${instanceText}${frameText}`); - } + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); - const stats = data.cachedStats[`imageId:${metadata.referencedImageId}`]; + // we don't have max yet for splines rois + // mappedAnnotations.forEach(mappedAnnotation => { + // const { unit, max, SeriesNumber } = mappedAnnotation; - const roundValues = values => { - if (Array.isArray(values)) { - return values.map(value => { - if (isNaN(value)) { - return value; - } - return utils.roundNumber(value); - }); - } - return isNaN(values) ? values : utils.roundNumber(values); - }; + // const maxStr = getStatisticDisplayString(max, unit, 'max'); - const findUnitForValue = (displayTextItems, value) => - displayTextItems.find(({ type, for: filter }) => type === 'unit' && filter.includes(value)) - ?.value; + // displayText.primary.push(maxStr); + // displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + // }); - const formatDisplayText = (displayName, result, unit) => - `${displayName}: ${Array.isArray(result) ? roundValues(result).join(', ') : roundValues(result)} ${unit}`; - - displayText.forEach(({ displayName, value, type }) => { - if (type === 'value') { - const result = stats[value]; - const unit = stats[findUnitForValue(displayText, value)] || ''; - displayTextArray.push(formatDisplayText(displayName, result, unit)); - } - }); - - return displayTextArray; + return displayText; } export default SplineROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts index b6997f3f5..e11f69870 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts @@ -1,8 +1,8 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; -import { getDisplayUnit } from './utils'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; - +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; const UltrasoundDirectional = { toAnnotation: measurement => {}, @@ -19,9 +19,10 @@ const UltrasoundDirectional = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; - + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); if (!metadata || !data) { console.warn('Length tool: Missing metadata or data'); return null; @@ -72,6 +73,8 @@ const UltrasoundDirectional = { data: data.cachedStats, type: getValueTypeFromToolType(toolName), getReport, + isLocked, + isVisible, }; }, }; @@ -167,11 +170,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio } function getDisplayText(mappedAnnotations, displaySet, customizationService) { - if (!mappedAnnotations || !mappedAnnotations.length) { - return ''; - } + const displayText = { + primary: [], + secondary: [], + }; - const displayText = []; + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; @@ -185,21 +191,23 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - const seriesText = `(S: ${SeriesNumber}${instanceText}${frameText})`; + const seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`; if (xValues === undefined || yValues === undefined) { return displayText; } if (isUnitless) { - displayText.push(`${utils.roundNumber(xValues[0], 2)} ${units[0]} ${seriesText}`); + displayText.primary.push(`${utils.roundNumber(xValues[0], 2)} ${units[0]}`); } else { const dist1 = Math.abs(xValues[1] - xValues[0]); const dist2 = Math.abs(yValues[1] - yValues[0]); - displayText.push(`${utils.roundNumber(dist1)} ${units[0]} ${seriesText}`); - displayText.push(`${utils.roundNumber(dist2)} ${units[1]} ${seriesText}`); + displayText.primary.push(`${utils.roundNumber(dist1)} ${units[0]}`); + displayText.primary.push(`${utils.roundNumber(dist2)} ${units[1]}`); } + displayText.secondary.push(seriesText); + return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts new file mode 100644 index 000000000..8a6ef2726 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts @@ -0,0 +1,5 @@ +import { locking } from '@cornerstonejs/tools/annotation'; + +export const getIsLocked = annotationUID => { + return locking.isAnnotationLocked(annotationUID); +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts new file mode 100644 index 000000000..fb310a2fc --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts @@ -0,0 +1,6 @@ +import { visibility } from '@cornerstonejs/tools/annotation'; + +export const getIsVisible = annotationUID => { + const isVisible = visibility.isAnnotationVisible(annotationUID); + return isVisible; +}; diff --git a/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts new file mode 100644 index 000000000..9fcf0f40b --- /dev/null +++ b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts @@ -0,0 +1,32 @@ +import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore'; +import { useLutPresentationStore } from '../../stores/useLutPresentationStore'; +import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; + +export function getViewportPresentations( + viewportId: string, + viewportOptions: AppTypes.ViewportGrid.GridViewportOptions +) { + const { lutPresentationStore } = useLutPresentationStore.getState(); + const { positionPresentationStore } = usePositionPresentationStore.getState(); + const { segmentationPresentationStore } = useSegmentationPresentationStore.getState(); + + // NOTE: this is the new viewport state, we should not get the presentationIds from the cornerstoneViewportService + // since that has the old viewport state + const { presentationIds } = viewportOptions; + + if (!presentationIds) { + return { + positionPresentation: null, + lutPresentation: null, + segmentationPresentation: null, + }; + } + + const { lutPresentationId, positionPresentationId, segmentationPresentationId } = presentationIds; + + return { + positionPresentation: positionPresentationStore[positionPresentationId], + lutPresentation: lutPresentationStore[lutPresentationId], + segmentationPresentation: segmentationPresentationStore[segmentationPresentationId], + }; +} diff --git a/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts b/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts similarity index 67% rename from extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts rename to extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts index b0f3e5796..edc7fc6f6 100644 --- a/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts +++ b/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts @@ -1,7 +1,7 @@ import { segmentation } from '@cornerstonejs/tools'; -function removeToolGroupSegmentationRepresentations(toolGroupId) { - const representations = segmentation.state.getSegmentationRepresentations(toolGroupId); +function removeViewportSegmentationRepresentations(viewportId) { + const representations = segmentation.state.getSegmentationRepresentations(viewportId); if (!representations || !representations.length) { return; @@ -9,10 +9,9 @@ function removeToolGroupSegmentationRepresentations(toolGroupId) { representations.forEach(representation => { segmentation.state.removeSegmentationRepresentation( - toolGroupId, representation.segmentationRepresentationUID ); }); } -export default removeToolGroupSegmentationRepresentations; +export default removeViewportSegmentationRepresentations; diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx index e3ad5c6c2..26bed59ef 100644 --- a/extensions/default/src/Components/SidePanelWithServices.tsx +++ b/extensions/default/src/Components/SidePanelWithServices.tsx @@ -5,7 +5,7 @@ import { Types } from '@ohif/core'; export type SidePanelWithServicesProps = { servicesManager: AppTypes.ServicesManager; side: 'left' | 'right'; - className: string; + className?: string; activeTabIndex: number; tabs: any; expandedWidth?: number; diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index a7628e80a..42c1c6033 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -209,6 +209,10 @@ function createDicomLocalApi(dicomLocalConfig) { return imageIds; }, getImageIdsForInstance({ instance, frame }) { + if (instance.imageId) { + return instance.imageId; + } + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; const storedInstance = DicomMetadataStore.getInstance( StudyInstanceUID, diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx index 22eea9bc8..ad292a268 100644 --- a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx +++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx @@ -2,8 +2,9 @@ import dcmjs from 'dcmjs'; import moment from 'moment'; import React, { useState, useMemo, useEffect } from 'react'; import { classes } from '@ohif/core'; -import { InputRange, Select, Typography, InputFilterText } from '@ohif/ui'; +import { InputFilterText } from '@ohif/ui'; import debounce from 'lodash.debounce'; +import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next'; import DicomTagTable from './DicomTagTable'; import './DicomTagBrowser.css'; @@ -57,7 +58,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => { return { value: displaySetInstanceUID, - label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`, + label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`, description: displayDate, }; }); @@ -107,62 +108,62 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => { }, []); return ( -
-
-
- - Series - -
+
+
+
+
+ Series +
{showInstanceList && ( -
- { - setInstanceNumber(parseInt(value)); +
+ + Instance Number ({instanceNumber} of {activeDisplaySet.images.length}) + + { + setInstanceNumber(value); }} - minValue={1} - maxValue={activeDisplaySet.images.length} + min={1} + max={activeDisplaySet.images.length} step={1} - inputClassName="w-full" - labelPosition="left" - trackColor={'#3a3f99'} + className="pt-4" />
)} +
+ + Search metadata + + +
-
-
- -
); diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js index 4bea9a074..ee13fa8c4 100644 --- a/extensions/default/src/DicomWebDataSource/qido.js +++ b/extensions/default/src/DicomWebDataSource/qido.js @@ -154,9 +154,11 @@ function mapParams(params, options = {}) { // Add more fields here if you want them in the result ].join(','); - const { supportsWildcard } = options; + const useWildcard = + params?.disableWildcard !== undefined ? !params.disableWildcard : options.supportsWildcard; + const withWildcard = value => { - return supportsWildcard && value ? `*${value}*` : value; + return useWildcard && value ? `*${value}*` : value; }; const parameters = { diff --git a/extensions/default/src/DicomWebDataSource/utils/getImageId.js b/extensions/default/src/DicomWebDataSource/utils/getImageId.js index 29877bd43..b3dcef097 100644 --- a/extensions/default/src/DicomWebDataSource/utils/getImageId.js +++ b/extensions/default/src/DicomWebDataSource/utils/getImageId.js @@ -29,6 +29,10 @@ export default function getImageId({ instance, frame, config, thumbnail = false return; } + if (instance.imageId && frame === undefined) { + return instance.imageId; + } + if (instance.url) { return instance.url; } diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx deleted file mode 100644 index 3df4c5cbe..000000000 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { useTranslation } from 'react-i18next'; -import { utils } from '@ohif/core'; -import { - MeasurementTable, - Dialog, - Input, - useViewportGrid, - ButtonEnums, - ActionButtons, -} from '@ohif/ui'; -import debounce from 'lodash.debounce'; - -import createReportDialogPrompt, { - CREATE_REPORT_DIALOG_RESPONSE, -} from './createReportDialogPrompt'; -import createReportAsync from '../Actions/createReportAsync'; -import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription'; -import { Separator } from '@ohif/ui-next'; - -const { downloadCSVReport } = utils; - -export default function PanelMeasurementTable({ - servicesManager, - commandsManager, - extensionManager, -}: withAppTypes): React.FunctionComponent { - const { t } = useTranslation('MeasurementTable'); - - const [viewportGrid, viewportGridService] = useViewportGrid(); - const { activeViewportId, viewports } = viewportGrid; - const { measurementService, uiDialogService, uiNotificationService, displaySetService } = - servicesManager.services; - const [displayMeasurements, setDisplayMeasurements] = useState([]); - - useEffect(() => { - const debouncedSetDisplayMeasurements = debounce(setDisplayMeasurements, 100); - // ~~ Initial - setDisplayMeasurements(_getMappedMeasurements(measurementService)); - - // ~~ Subscription - const added = measurementService.EVENTS.MEASUREMENT_ADDED; - const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED; - const updated = measurementService.EVENTS.MEASUREMENT_UPDATED; - const removed = measurementService.EVENTS.MEASUREMENT_REMOVED; - const cleared = measurementService.EVENTS.MEASUREMENTS_CLEARED; - const subscriptions = []; - - [added, addedRaw, updated, removed, cleared].forEach(evt => { - subscriptions.push( - measurementService.subscribe(evt, () => { - debouncedSetDisplayMeasurements(_getMappedMeasurements(measurementService)); - }).unsubscribe - ); - }); - - return () => { - subscriptions.forEach(unsub => { - unsub(); - }); - debouncedSetDisplayMeasurements.cancel(); - }; - }, []); - - async function exportReport() { - const measurements = measurementService.getMeasurements(); - - downloadCSVReport(measurements, measurementService); - } - - async function clearMeasurements() { - measurementService.clearMeasurements(); - } - - async function createReport(): Promise { - // filter measurements that are added to the active study - const activeViewport = viewports.get(activeViewportId); - const measurements = measurementService.getMeasurements(); - const displaySet = displaySetService.getDisplaySetByUID( - activeViewport.displaySetInstanceUIDs[0] - ); - const trackedMeasurements = measurements.filter( - m => displaySet.StudyInstanceUID === m.referenceStudyUID - ); - - if (trackedMeasurements.length <= 0) { - uiNotificationService.show({ - title: 'No Measurements', - message: 'No Measurements are added to the current Study.', - type: 'info', - duration: 3000, - }); - return; - } - - const promptResult = await createReportDialogPrompt(uiDialogService, { - extensionManager, - }); - - if (promptResult.action === CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT) { - const dataSources = extensionManager.getDataSources(promptResult.dataSourceName); - const dataSource = dataSources[0]; - - const SeriesDescription = - // isUndefinedOrEmpty - promptResult.value === undefined || promptResult.value === '' - ? 'Research Derived Series' // default - : promptResult.value; // provided value - - // Reuse an existing series having the same series description to avoid - // creating too many series instances. - const options = findSRWithSameSeriesDescription(SeriesDescription, displaySetService); - - const getReport = async () => { - return commandsManager.runCommand( - 'storeMeasurements', - { - measurementData: trackedMeasurements, - dataSource, - additionalFindingTypes: ['ArrowAnnotate'], - options, - }, - 'CORNERSTONE_STRUCTURED_REPORT' - ); - }; - - return createReportAsync({ servicesManager, getReport }); - } - } - - const jumpToImage = ({ uid, isActive }) => { - measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); - - onMeasurementItemClickHandler({ uid, isActive }); - }; - - const onMeasurementItemEditHandler = ({ uid, isActive }) => { - const measurement = measurementService.getMeasurement(uid); - //Todo: why we are jumping to image? - // jumpToImage({ id, isActive }); - - const onSubmitHandler = ({ action, value }) => { - switch (action.id) { - case 'save': { - measurementService.update( - uid, - { - ...measurement, - ...value, - }, - true - ); - } - } - uiDialogService.dismiss({ id: 'enter-annotation' }); - }; - - uiDialogService.create({ - id: 'enter-annotation', - centralize: true, - isDraggable: false, - showOverlay: true, - content: Dialog, - contentProps: { - title: 'Annotation', - noCloseButton: true, - value: { label: measurement.label || '' }, - body: ({ value, setValue }) => { - const onChangeHandler = event => { - event.persist(); - setValue(value => ({ ...value, label: event.target.value })); - }; - - const onKeyPressHandler = event => { - if (event.key === 'Enter') { - onSubmitHandler({ value, action: { id: 'save' } }); - } - }; - return ( - - ); - }, - actions: [ - { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, - { id: 'save', text: 'Save', type: ButtonEnums.type.primary }, - ], - onSubmit: onSubmitHandler, - }, - }); - }; - - const onMeasurementItemClickHandler = ({ uid, isActive }) => { - if (!isActive) { - const measurements = [...displayMeasurements]; - const measurement = measurements.find(m => m.uid === uid); - - measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true)); - measurement.isActive = true; - setDisplayMeasurements(measurements); - } - }; - - return ( - <> -
- -
-
- -
- - ); -} - -PanelMeasurementTable.propTypes = { - servicesManager: PropTypes.object.isRequired, -}; - -function _getMappedMeasurements(measurementService) { - const measurements = measurementService.getMeasurements(); - - const mappedMeasurements = measurements.map((m, index) => - _mapMeasurementToDisplay(m, index, measurementService.VALUE_TYPES) - ); - - return mappedMeasurements; -} - -/** - * Map the measurements to the display text. - * Adds finding and site information to the displayText and/or label, - * and provides as 'displayText' and 'label', while providing the original - * values as baseDisplayText and baseLabel - */ -function _mapMeasurementToDisplay(measurement, index, types) { - const { - displayText: baseDisplayText, - uid, - label: baseLabel, - type, - selected, - findingSites, - finding, - } = measurement; - - const firstSite = findingSites?.[0]; - const label = baseLabel || finding?.text || firstSite?.text || '(empty)'; - let displayText = baseDisplayText || []; - if (findingSites) { - const siteText = []; - findingSites.forEach(site => { - if (site?.text !== label) { - siteText.push(site.text); - } - }); - displayText = [...siteText, ...displayText]; - } - if (finding && finding?.text !== label) { - displayText = [finding.text, ...displayText]; - } - - return { - uid, - label, - baseLabel, - measurementType: type, - displayText, - baseDisplayText, - isActive: selected, - finding, - findingSites, - }; -} diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index 9fa050b48..4dd7225b2 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -1,5 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { useState, useEffect } from 'react'; import { useImageViewer, useViewportGrid } from '@ohif/ui'; import { StudyBrowser } from '@ohif/ui-next'; import { utils } from '@ohif/core'; @@ -80,7 +79,7 @@ function PanelStudyBrowser({ uiNotificationService.show({ title: 'Thumbnail Double Click', message: 'The selected display sets could not be added to the viewport.', - type: 'info', + type: 'error', duration: 3000, }); } @@ -198,7 +197,7 @@ function PanelStudyBrowser({ // if (!hasLoadedViewports) { // return; // } - const { displaySetsAdded, options } = data; + const { displaySetsAdded } = data; displaySetsAdded.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); @@ -319,16 +318,6 @@ function PanelStudyBrowser({ ); } -PanelStudyBrowser.propTypes = { - servicesManager: PropTypes.object.isRequired, - dataSource: PropTypes.shape({ - getImageIdsForDisplaySet: PropTypes.func.isRequired, - }).isRequired, - getImageSrc: PropTypes.func.isRequired, - getStudiesForPatientByMRN: PropTypes.func.isRequired, - requestDisplaySetCreationForStudy: PropTypes.func.isRequired, -}; - export default PanelStudyBrowser; /** diff --git a/extensions/default/src/Panels/createReportDialogPrompt.tsx b/extensions/default/src/Panels/createReportDialogPrompt.tsx index 55c921502..4e8dfa304 100644 --- a/extensions/default/src/Panels/createReportDialogPrompt.tsx +++ b/extensions/default/src/Panels/createReportDialogPrompt.tsx @@ -1,11 +1,7 @@ import React from 'react'; import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui'; - -export const CREATE_REPORT_DIALOG_RESPONSE = { - CANCEL: 0, - CREATE_REPORT: 1, -}; +import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES'; export default function CreateReportDialogPrompt(uiDialogService, { extensionManager }) { return new Promise(function (resolve, reject) { @@ -16,7 +12,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan uiDialogService.dismiss({ id: dialogId }); // Notify of cancel action resolve({ - action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL, + action: PROMPT_RESPONSES.CANCEL, value: undefined, dataSourceName: undefined, }); @@ -32,14 +28,14 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan switch (action.id) { case 'save': resolve({ - action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT, + action: PROMPT_RESPONSES.CREATE_REPORT, value: value.label, dataSourceName: value.dataSourceName, }); break; case 'cancel': resolve({ - action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL, + action: PROMPT_RESPONSES.CANCEL, value: undefined, dataSourceName: undefined, }); @@ -90,7 +86,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan if (event.key === 'Enter') { uiDialogService.dismiss({ id: dialogId }); resolve({ - action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT, + action: PROMPT_RESPONSES.CREATE_REPORT, value: value.label, }); } @@ -102,7 +98,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan { - event.persist(); - setValue(value => ({ ...value, value: event.target.value })); - }} - onKeyPress={event => { - if (event.key === 'Enter') { - onSubmitHandler({ value, action: { id: 'save' } }); - } - }} - /> - ); - }, - }, - }); - } -} diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 9035eace9..d71d8cfe6 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,10 +32,11 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^1.86.0", - "@cornerstonejs/tools": "^1.86.0", + "@cornerstonejs/core": "^2.1.13", + "@cornerstonejs/tools": "^2.1.13", "@ohif/core": "3.9.0-beta.105", "@ohif/extension-cornerstone-dicom-sr": "3.9.0-beta.105", + "@ohif/extension-default": "3.9.0-beta.104", "@ohif/ui": "3.9.0-beta.105", "classnames": "^2.3.2", "dcmjs": "*", diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index 7e89712ee..21b9efe9b 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -1,17 +1,16 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Machine } from 'xstate'; import { useMachine } from '@xstate/react'; import { useViewportGrid } from '@ohif/ui'; +import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default'; import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine'; import promptBeginTracking from './promptBeginTracking'; import promptTrackNewSeries from './promptTrackNewSeries'; import promptTrackNewStudy from './promptTrackNewStudy'; -import promptSaveReport from './promptSaveReport'; import promptHydrateStructuredReport from './promptHydrateStructuredReport'; import hydrateStructuredReport from './hydrateStructuredReport'; import { useAppConfig } from '@state'; -import promptLabelAnnotation from './promptLabelAnnotation'; const TrackedMeasurementsContext = React.createContext(); TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; @@ -96,17 +95,18 @@ function TrackedMeasurementsContextProvider( const trackedMeasurement = trackedMeasurements[0]; const referencedDisplaySetUID = trackedMeasurement.displaySetInstanceUID; - const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId); - const imageIndex = viewport.getCurrentImageIdIndex(); + + // update the previously stored positionPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will be in the correct position zoom and pan + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId: activeViewportId, + displaySetInstanceUID: referencedDisplaySetUID, + }); viewportGridService.setDisplaySetsForViewport({ viewportId: activeViewportId, displaySetInstanceUIDs: [referencedDisplaySetUID], - viewportOptions: { - initialImageOptions: { - index: imageIndex, - }, - }, }); }, showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => { @@ -196,7 +196,9 @@ function TrackedMeasurementsContextProvider( // - Fix viewport border resize // - created/destroyed hooks for extensions (cornerstone measurement subscriptions in it's `init`) - const measurementTrackingMachine = Machine(machineConfiguration, machineOptions); + const measurementTrackingMachine = useMemo(() => { + return Machine(machineConfiguration, machineOptions); + }, []); // Empty dependency array ensures this is only created once const [trackedMeasurements, sendTrackedMeasurementsEvent] = useMachine( measurementTrackingMachine diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index d53408e3c..e23de0c62 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -1,4 +1,3 @@ -import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr'; import { assign } from 'xstate'; const RESPONSE = { diff --git a/extensions/measurement-tracking/src/getPanelModule.tsx b/extensions/measurement-tracking/src/getPanelModule.tsx index ef09f59ad..f2bc44cf9 100644 --- a/extensions/measurement-tracking/src/getPanelModule.tsx +++ b/extensions/measurement-tracking/src/getPanelModule.tsx @@ -24,7 +24,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }): /> ), }, - { name: 'trackedMeasurements', iconName: 'tab-linear', diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx new file mode 100644 index 000000000..7ac0ebcc1 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useState } from 'react'; +import { PanelMeasurement } from '@ohif/extension-cornerstone'; +import { useViewportGrid } from '@ohif/ui'; +import { StudySummary } from '@ohif/ui-next'; +import { Button, Icons } from '@ohif/ui-next'; +import { DicomMetadataStore, utils } from '@ohif/core'; +import { useTrackedMeasurements } from '../getContextModule'; +import { useTranslation } from 'react-i18next'; + +const { downloadCSVReport, formatDate } = utils; + +const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { + key: undefined, // + date: '', // '07-Sep-2010', + modality: '', // 'CT', + description: '', // 'CHEST/ABD/PELVIS W CONTRAST', +}; + +function PanelMeasurementTableTracking({ + servicesManager, + extensionManager, + commandsManager, +}: withAppTypes) { + const [viewportGrid] = useViewportGrid(); + const { t } = useTranslation('MeasurementTable'); + const { measurementService, customizationService } = servicesManager.services; + const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); + const { trackedStudy, trackedSeries } = trackedMeasurements.context; + const [displayStudySummary, setDisplayStudySummary] = useState( + DISPLAY_STUDY_SUMMARY_INITIAL_VALUE + ); + + useEffect(() => { + const updateDisplayStudySummary = async () => { + if (trackedMeasurements.matches('tracking') && trackedStudy) { + const studyMeta = DicomMetadataStore.getStudy(trackedStudy); + if (!studyMeta || !studyMeta.series || studyMeta.series.length === 0) { + console.debug('Study metadata not available'); + return; + } + + const instanceMeta = studyMeta.series[0].instances[0]; + const { StudyDate, StudyDescription } = instanceMeta; + + const modalities = new Set(); + studyMeta.series.forEach(series => { + if (trackedSeries.includes(series.SeriesInstanceUID)) { + modalities.add(series.instances[0].Modality); + } + }); + const modality = Array.from(modalities).join('/'); + + setDisplayStudySummary(prevSummary => { + if (prevSummary.key !== trackedStudy) { + return { + key: trackedStudy, + date: StudyDate, + modality, + description: StudyDescription, + }; + } + return prevSummary; + }); + } else if (!trackedStudy) { + setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); + } + }; + + updateDisplayStudySummary(); + }, [trackedMeasurements, trackedStudy, trackedSeries]); + + const { disableEditing } = customizationService.getCustomization( + 'PanelMeasurement.disableEditing', + { + id: 'default.disableEditing', + disableEditing: false, + } + ); + + return ( + <> + {displayStudySummary.key && ( + + )} + + trackedStudy === measurement.referenceStudyUID && + trackedSeries.includes(measurement.referenceSeriesUID) + } + customHeader={({ additionalFindings, measurements }) => { + const disabled = additionalFindings.length === 0 && measurements.length === 0; + + if (disableEditing || disabled) { + return null; + } + + return ( +
+
+ + +
+
+ ); + }} + >
+ + ); +} + +export default PanelMeasurementTableTracking; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx deleted file mode 100644 index 6efa62ac2..000000000 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { StudySummary, MeasurementTable, useViewportGrid, ActionButtons } from '@ohif/ui'; -import { DicomMetadataStore, utils } from '@ohif/core'; -import { useDebounce } from '@hooks'; -import { useAppConfig } from '@state'; -import debounce from 'lodash.debounce'; -import { useTranslation } from 'react-i18next'; -import { Separator } from '@ohif/ui-next'; - -import { useTrackedMeasurements } from '../../getContextModule'; - -const { downloadCSVReport } = utils; -const { formatDate } = utils; - -const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { - key: undefined, // - date: '', // '07-Sep-2010', - modality: '', // 'CT', - description: '', // 'CHEST/ABD/PELVIS W CONTRAST', -}; - -function PanelMeasurementTableTracking({ servicesManager, extensionManager }: withAppTypes) { - const [viewportGrid] = useViewportGrid(); - const { t } = useTranslation('MeasurementTable'); - const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString()); - const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200); - const { measurementService, uiDialogService, displaySetService, customizationService } = - servicesManager.services; - const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); - const { trackedStudy, trackedSeries } = trackedMeasurements.context; - const [displayStudySummary, setDisplayStudySummary] = useState( - DISPLAY_STUDY_SUMMARY_INITIAL_VALUE - ); - const [displayMeasurements, setDisplayMeasurements] = useState([]); - const measurementsPanelRef = useRef(null); - const [appConfig] = useAppConfig(); - - useEffect(() => { - const measurements = measurementService.getMeasurements(); - const mappedMeasurements = measurements.map(m => - _mapMeasurementToDisplay(m, displaySetService) - ); - setDisplayMeasurements(mappedMeasurements); - // eslint-ignore-next-line - }, [ - measurementService, - displaySetService, - trackedStudy, - trackedSeries, - debouncedMeasurementChangeTimestamp, - ]); - - // ~~ DisplayStudySummary - useEffect(() => { - const updateDisplayStudySummary = async () => { - if (trackedMeasurements.matches('tracking')) { - const StudyInstanceUID = trackedStudy; - const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID); - const instanceMeta = studyMeta.series[0].instances[0]; - const { StudyDate, StudyDescription } = instanceMeta; - - const modalities = new Set(); - studyMeta.series.forEach(series => { - if (trackedSeries.includes(series.SeriesInstanceUID)) { - modalities.add(series.instances[0].Modality); - } - }); - const modality = Array.from(modalities).join('/'); - - if (displayStudySummary.key !== StudyInstanceUID) { - setDisplayStudySummary({ - key: StudyInstanceUID, - date: StudyDate, // TODO: Format: '07-Sep-2010' - modality, - description: StudyDescription, - }); - } - } else if (trackedStudy === '' || trackedStudy === undefined) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); - } - }; - updateDisplayStudySummary(); - }, [displayStudySummary.key, trackedSeries, trackedMeasurements, trackedStudy]); - - // TODO: Better way to consolidated, debounce, check on change? - // Are we exposing the right API for measurementService? - // This watches for ALL measurementService changes. It updates a timestamp, - // which is debounced. After a brief period of inactivity, this triggers - // a re-render where we grab up-to-date measurements - useEffect(() => { - const added = measurementService.EVENTS.MEASUREMENT_ADDED; - const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED; - const updated = measurementService.EVENTS.MEASUREMENT_UPDATED; - const removed = measurementService.EVENTS.MEASUREMENT_REMOVED; - const cleared = measurementService.EVENTS.MEASUREMENTS_CLEARED; - const subscriptions = []; - - [added, addedRaw, updated, removed, cleared].forEach(evt => { - subscriptions.push( - measurementService.subscribe(evt, () => { - setMeasurementsUpdated(Date.now().toString()); - if (evt === added) { - debounce(() => { - measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight; - }, 300)(); - } - }).unsubscribe - ); - }); - - return () => { - subscriptions.forEach(unsub => { - unsub(); - }); - }; - }, [measurementService, sendTrackedMeasurementsEvent]); - - async function exportReport() { - const measurements = measurementService.getMeasurements(); - const trackedMeasurements = measurements.filter( - m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) - ); - downloadCSVReport(trackedMeasurements); - } - - const jumpToImage = ({ uid, isActive }) => { - measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); - onMeasurementItemClickHandler({ uid, isActive }); - }; - - const onMeasurementItemEditHandler = ({ uid, isActive }) => { - jumpToImage({ uid, isActive }); - const labelConfig = customizationService.get('measurementLabels'); - const measurement = measurementService.getMeasurement(uid); - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.common' - ); - const { showLabelAnnotationPopup } = utilityModule.exports; - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then( - (val: Map) => { - measurementService.update( - uid, - { - ...val, - }, - true - ); - } - ); - }; - - const onMeasurementItemClickHandler = ({ uid, isActive }) => { - if (!isActive) { - const measurements = [...displayMeasurements]; - const measurement = measurements.find(m => m.uid === uid); - measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true)); - measurement.isActive = true; - setDisplayMeasurements(measurements); - } - }; - - const displayMeasurementsWithoutFindings = displayMeasurements.filter( - dm => - dm.measurementType !== measurementService.VALUE_TYPES.POINT && - dm.referencedImageId && - trackedStudy === dm.referenceStudyUID && - trackedSeries.includes(dm.referenceSeriesUID) - ); - const additionalFindings = displayMeasurements.filter( - dm => - dm.measurementType === measurementService.VALUE_TYPES.POINT && - dm.referencedImageId && - trackedStudy === dm.referenceStudyUID && - trackedSeries.includes(dm.referenceSeriesUID) - ); - const nonAcquisitionMeasurements = displayMeasurements.filter( - dm => - dm.referencedImageId == null || - trackedStudy !== dm.referenceStudyUID || - trackedSeries.includes(dm.referenceSeriesUID) - ); - const disabled = - additionalFindings.length === 0 && - displayMeasurementsWithoutFindings.length === 0 && - nonAcquisitionMeasurements.length === 0; - - return ( - <> -
- {displayStudySummary.key && ( - - )} - - {additionalFindings.length !== 0 && ( - - )} - {nonAcquisitionMeasurements.length !== 0 && ( - - )} -
- {!appConfig?.disableEditing && ( -
- { - sendTrackedMeasurementsEvent('SAVE_REPORT', { - viewportId: viewportGrid.activeViewportId, - isBackupSave: true, - }); - }, - }, - ]} - disabled={disabled} - /> -
- )} - - ); -} - -PanelMeasurementTableTracking.propTypes = { - servicesManager: PropTypes.shape({ - services: PropTypes.shape({ - measurementService: PropTypes.shape({ - getMeasurements: PropTypes.func.isRequired, - VALUE_TYPES: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, -}; - -// TODO: This could be a measurementService mapper -function _mapMeasurementToDisplay(measurement, displaySetService) { - const { referenceSeriesUID } = measurement; - - // TODO: We don't deal with multiframe well yet, would need to update - // This in OHIF-312 when we add FrameIndex to measurements - - const { - displayText: baseDisplayText, - uid, - label: baseLabel, - type, - selected, - findingSites, - finding, - referencedImageId, - } = measurement; - - const firstSite = findingSites?.[0]; - const label = baseLabel || finding?.text || firstSite?.text || '(empty)'; - let displayText = baseDisplayText || []; - if (findingSites) { - const siteText = []; - findingSites.forEach(site => { - if (site?.text !== label) { - siteText.push(site.text); - } - }); - displayText = [...siteText, ...displayText]; - } - if (finding && finding?.text !== label) { - displayText = [finding.text, ...displayText]; - } - - return { - uid, - label, - baseLabel, - measurementType: type, - displayText, - baseDisplayText, - isActive: selected, - finding, - findingSites, - referencedImageId, - }; -} - -export default PanelMeasurementTableTracking; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 97b9ee9aa..16de940d7 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -12,7 +12,17 @@ import { PanelStudyBrowserTrackingHeader } from './PanelStudyBrowserTrackingHead import { defaultActionIcons, defaultViewPresets } from './constants'; const { formatDate, createStudyBrowserTabs } = utils; - +const thumbnailNoImageModalities = [ + 'SR', + 'SEG', + 'SM', + 'RTSTRUCT', + 'RTPLAN', + 'RTDOSE', + 'DOC', + 'OT', + 'PMAP', +]; /** * * @param {*} param0 @@ -94,7 +104,7 @@ function PanelStudyBrowserTracking({ title: 'Thumbnail Double Click', 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, }); } @@ -171,7 +181,11 @@ function PanelStudyBrowserTracking({ return; } - const currentDisplaySets = displaySetService.activeDisplaySets; + let currentDisplaySets = displaySetService.activeDisplaySets; + // filter non based on the list of modalities that are supported by cornerstone + currentDisplaySets = currentDisplaySets.filter( + ds => !thumbnailNoImageModalities.includes(ds.Modality) + ); if (!currentDisplaySets.length) { return; @@ -677,18 +691,6 @@ function _mapDisplaySets( return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; } -const thumbnailNoImageModalities = [ - 'SR', - 'SEG', - 'SM', - 'RTSTRUCT', - 'RTPLAN', - 'RTDOSE', - 'DOC', - 'OT', - 'PMAP', -]; - function _getComponentType(ds) { if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { return 'thumbnailNoImage'; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index f0c7cdac0..be40e23b5 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -8,7 +8,9 @@ import { useTrackedMeasurements } from './../getContextModule'; import { BaseVolumeViewport, Enums } from '@cornerstonejs/core'; import { useTranslation } from 'react-i18next'; -function TrackedCornerstoneViewport(props: withAppTypes) { +function TrackedCornerstoneViewport( + props: withAppTypes<{ viewportId: string; displaySets: AppTypes.DisplaySet[] }> +) { const { displaySets, viewportId, servicesManager, extensionManager } = props; const { @@ -193,7 +195,7 @@ function TrackedCornerstoneViewport(props: withAppTypes) { viewportId === activeViewportId ); - viewportActionCornersService.setComponents([ + viewportActionCornersService.addComponents([ { viewportId, id: 'viewportStatusComponent', @@ -305,7 +307,7 @@ const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) => return ( switchMeasurement(direction)} - className={isActiveViewport ? 'visible' : 'invisible group-hover:visible'} + className={isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'} > ); }; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx index 1e429c1f8..3f3f77349 100644 --- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx @@ -1,171 +1,76 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { Icon, ActionButtons } from '@ohif/ui'; -import { useTranslation } from 'react-i18next'; -import { eventTarget } from '@cornerstonejs/core'; -import { Enums } from '@cornerstonejs/tools'; +import React, { useEffect } from 'react'; +import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone'; import { handleROIThresholding } from '../../utils/handleROIThresholding'; +import { debounce } from '@ohif/core/src/utils'; export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager, }: withAppTypes) { - const { segmentationService, uiNotificationService } = servicesManager.services; - const { t } = useTranslation('PanelSUVExport'); + const { segmentationService } = servicesManager.services; + const { segmentationsWithRepresentations: segmentationsInfo } = + useActiveViewportSegmentationRepresentations({ servicesManager }); - const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); - const [activeSegmentation, setActiveSegmentation] = useState(null); - - /** - * Update UI based on segmentation changes (added, removed, updated) - */ useEffect(() => { - // ~~ Subscription - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; - const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED; - const subscriptions = []; + const segmentationIds = segmentationsInfo.map( + segmentationInfo => segmentationInfo.segmentation.segmentationId + ); - [added, updated, removed].forEach(evt => { - const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); - setSegmentations(segmentations); - - const activeSegmentation = segmentations.filter(seg => seg.isActive); - setActiveSegmentation(activeSegmentation[0]); - }); - - subscriptions.push(unsubscribe); - }); - - return () => { - subscriptions.forEach(unsub => { - unsub(); - }); + const initialRun = async () => { + for (const segmentationId of segmentationIds) { + await handleROIThresholding({ + segmentationId, + commandsManager, + segmentationService, + }); + } }; + + initialRun(); }, []); useEffect(() => { - const callback = async evt => { - const { detail } = evt; - const { segmentationId } = detail; - - if (!segmentationId) { - return; - } - + const debouncedHandleROIThresholding = debounce(async eventDetail => { + const { segmentationId } = eventDetail; await handleROIThresholding({ segmentationId, commandsManager, segmentationService, }); + }, 100); - const segmentation = segmentationService.getSegmentation(segmentationId); - - const { cachedStats } = segmentation; - if (!cachedStats) { - return; - } - - // segment 1 - const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak; - - if (Number.isNaN(suvPeak)) { - uiNotificationService.show({ - title: 'SUV Peak', - message: 'Segmented volume does not allow SUV Peak calculation', - type: 'warning', - }); - } + const dataModifiedCallback = eventDetail => { + debouncedHandleROIThresholding(eventDetail); }; - eventTarget.addEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback, 250); + const dataModifiedSubscription = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + dataModifiedCallback + ); return () => { - eventTarget.removeEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback); + dataModifiedSubscription.unsubscribe(); }; - }, []); + }, [commandsManager, segmentationService]); - if (!activeSegmentation) { - return null; - } - - const tmtvValue = activeSegmentation.cachedStats?.tmtv?.value || null; - const config = activeSegmentation.cachedStats?.tmtv?.config || {}; - - const actions = [ - { - label: 'Export CSV', - onClick: () => { - commandsManager.runCommand('exportTMTVReportCSV', { - segmentations, - tmtv: tmtvValue, - config, - }); - }, - disabled: tmtvValue === null, - }, - { - label: 'Export RT Report', - onClick: () => { - commandsManager.runCommand('createTMTVRTReport'); - }, - disabled: tmtvValue === null, - }, - ]; + // Find the first segmentation with a TMTV value since all of them have the same value + const tmtvSegmentation = segmentationsInfo.find( + info => info.segmentation.cachedStats?.tmtv !== undefined + ); + const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv; return ( - <> -
-
- {tmtvValue !== null ? ( -
- - {'TMTV:'} - -
{`${tmtvValue} mL`}
-
- ) : null} -
- +
+
+ {tmtvValue !== null && tmtvValue !== undefined ? ( +
+ + {'TMTV:'} + +
{`${tmtvValue.toFixed(3)} mL`}
-
+ ) : null}
-
{ - // navigate to a url in a new tab - window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank'); - }} - > - - {'User Guide'} -
- +
); } - -PanelRoiThresholdSegmentation.propTypes = { - commandsManager: PropTypes.shape({ - runCommand: PropTypes.func.isRequired, - }), - servicesManager: PropTypes.shape({ - services: PropTypes.shape({ - segmentationService: PropTypes.shape({ - getSegmentation: PropTypes.func.isRequired, - getSegmentations: PropTypes.func.isRequired, - toggleSegmentationVisibility: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - EVENTS: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, -}; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx index 1ec15652e..605d17bbb 100644 --- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx @@ -9,14 +9,10 @@ function segmentationItemEditHandler({ id, servicesManager }: withAppTypes) { const onSubmitHandler = ({ action, value }) => { switch (action.id) { case 'save': { - segmentationService.addOrUpdateSegmentation( - { - ...segmentation, - ...value, - }, - false, // don't suppress event - true // it should update cornerstone - ); + segmentationService.addOrUpdateSegmentation({ + ...segmentation, + ...value, + }); } } uiDialogService.dismiss({ id: 'enter-annotation' }); diff --git a/extensions/tmtv/src/Panels/PanelTMTV.tsx b/extensions/tmtv/src/Panels/PanelTMTV.tsx new file mode 100644 index 000000000..753a8ccc9 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelTMTV.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { + PanelSegmentation, + useActiveViewportSegmentationRepresentations, +} from '@ohif/extension-cornerstone'; +import { Button, Icons } from '@ohif/ui-next'; + +export default function PanelTMTV({ + servicesManager, + commandsManager, + extensionManager, + configuration, +}: withAppTypes) { + return ( + <> + + + + + ); +} + +const ExportCSV = ({ servicesManager, commandsManager }: withAppTypes) => { + const { segmentationsWithRepresentations: representations } = + useActiveViewportSegmentationRepresentations({ servicesManager }); + + const tmtv = representations[0]?.segmentation.cachedStats?.tmtv; + + const segmentations = representations.map(representation => representation.segmentation); + + if (!segmentations.length) { + return null; + } + + return ( +
+ +
+ ); +}; diff --git a/extensions/tmtv/src/Panels/RectangleROIOptions.tsx b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx index 85d13821a..19101ba06 100644 --- a/extensions/tmtv/src/Panels/RectangleROIOptions.tsx +++ b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx @@ -75,7 +75,7 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes) }, [selectedSegmentationId, config]); useEffect(() => { - const segmentations = segmentationService.getSegmentations(); + const segmentations = segmentationService.getSegmentationRepresentations(); if (!segmentations.length) { return; @@ -90,13 +90,12 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes) */ useEffect(() => { // ~~ Subscription - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; + const updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED; const subscriptions = []; - [added, updated].forEach(evt => { + [updated].forEach(evt => { const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); + const segmentations = segmentationService.getSegmentationRepresentations(); if (!segmentations.length) { return; diff --git a/extensions/tmtv/src/commandsModule.ts b/extensions/tmtv/src/commandsModule.ts index 8ea6ebd1e..35e6f67ef 100644 --- a/extensions/tmtv/src/commandsModule.ts +++ b/extensions/tmtv/src/commandsModule.ts @@ -1,23 +1,23 @@ -import { vec3 } from 'gl-matrix'; import OHIF from '@ohif/core'; import * as cs from '@cornerstonejs/core'; import * as csTools from '@cornerstonejs/tools'; import { classes } from '@ohif/core'; import getThresholdValues from './utils/getThresholdValue'; -import calculateTMTV from './utils/calculateTMTV'; import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport'; import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet'; import { getWebWorkerManager } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; + +const { SegmentationRepresentations } = Enums; const metadataProvider = classes.MetadataProvider; const ROI_THRESHOLD_MANUAL_TOOL_IDS = [ 'RectangleROIStartEndThreshold', 'RectangleROIThreshold', - 'CircleROIStartEndThreshold' + 'CircleROIStartEndThreshold', ]; -const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap; const workerManager = getWebWorkerManager(); @@ -36,6 +36,28 @@ const workerFn = () => { }); }; +function getVolumesFromSegmentation(segmentationId) { + const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId); + const labelmapData = csSegmentation.representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + + const { volumeId, referencedVolumeId } = labelmapData; + const labelmapVolume = cs.cache.getVolume(volumeId); + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + + return { labelmapVolume, referencedVolume }; +} + +function getLabelmapVolumeFromSegmentation(segmentation) { + const { representationData } = segmentation; + const { volumeId } = representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + + return cs.cache.getVolume(volumeId); +} + const commandsModule = ({ servicesManager, commandsManager, extensionManager }: withAppTypes) => { const { viewportGridService, @@ -60,20 +82,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: return enabledElement; } - function _getMatchedViewportsToolGroupIds() { - const { viewportMatchDetails } = hangingProtocolService.getMatchDetails(); - const toolGroupIds = []; - viewportMatchDetails.forEach(viewport => { - const { viewportOptions } = viewport; - const { toolGroupId } = viewportOptions; - if (toolGroupIds.indexOf(toolGroupId) === -1) { - toolGroupIds.push(toolGroupId); - } - }); - - return toolGroupIds; - } - function _getAnnotationsSelectedByToolNames(toolNames) { return toolNames.reduce((allAnnotationUIDs, toolName) => { const annotationUIDs = @@ -102,7 +110,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: } ptDisplaySet = displaySets.find(displaySet => displaySet.Modality === 'PT'); - if (ptDisplaySet) { break; } @@ -142,55 +149,48 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: createNewLabelmapFromPT: async ({ label }) => { // Create a segmentation of the same resolution as the source data // using volumeLoader.createAndCacheDerivedVolume. + const { viewportMatchDetails } = hangingProtocolService.getMatchDetails(); const ptDisplaySet = actions.getMatchingPTDisplaySet({ viewportMatchDetails, }); + let withPTViewportId = null; + + for (const [viewportId, { displaySetsInfo }] of viewportMatchDetails.entries()) { + const isPT = displaySetsInfo.some( + ({ displaySetInstanceUID }) => + displaySetInstanceUID === ptDisplaySet.displaySetInstanceUID + ); + + if (isPT) { + withPTViewportId = viewportId; + break; + } + } + if (!ptDisplaySet) { uiNotificationService.error('No matching PT display set found'); return; } - const currentSegmentations = segmentationService.getSegmentations(); + const currentSegmentations = + segmentationService.getSegmentationRepresentations(withPTViewportId); - const segmentationId = await segmentationService.createSegmentationForDisplaySet( - ptDisplaySet.displaySetInstanceUID, - { label: `Segmentation ${currentSegmentations.length + 1}` } - ); + const displaySet = displaySetService.getDisplaySetByUID(ptDisplaySet.displaySetInstanceUID); - // Add Segmentation to all toolGroupIds in the viewer - const toolGroupIds = _getMatchedViewportsToolGroupIds(); - const representationType = LABELMAP; - - for (const toolGroupId of toolGroupIds) { - const hydrateSegmentation = true; - await segmentationService.addSegmentationRepresentationToToolGroup( - toolGroupId, - segmentationId, - hydrateSegmentation, - representationType - ); - - segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId); - } - - segmentationService.addSegment(segmentationId, { - segmentIndex: 1, - properties: { - label: 'Segment 1', - }, + const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, { + label: `Segmentation ${currentSegmentations.length + 1}`, + segments: { 1: { label: 'Segment 1', active: true } }, }); + + segmentationService.addSegmentationRepresentation(withPTViewportId, { + segmentationId, + }); + return segmentationId; }, - setSegmentationActiveForToolGroups: ({ segmentationId }) => { - const toolGroupIds = _getMatchedViewportsToolGroupIds(); - - toolGroupIds.forEach(toolGroupId => { - segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId); - }); - }, thresholdSegmentationByRectangleROITool: ({ segmentationId, config, segmentIndex }) => { const segmentation = csTools.segmentation.state.getSegmentation(segmentationId); @@ -201,12 +201,12 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const ctDisplaySet = matchDetails.get('ctDisplaySet'); const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id - const { volumeId: segVolumeId } = representationData[LABELMAP]; + const { volumeId: segVolumeId } = representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; const { referencedVolumeId } = cs.cache.getVolume(segVolumeId); - const annotationUIDs = _getAnnotationsSelectedByToolNames( - ROI_THRESHOLD_MANUAL_TOOL_IDS - ); + const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS); if (annotationUIDs.length === 0) { uiNotificationService.show({ @@ -284,16 +284,21 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: { overwrite: true, segmentIndex } ); }, - calculateSuvPeak: async ({ labelmap, segmentIndex }) => { + calculateSuvPeak: async ({ segmentationId, segmentIndex }) => { + const segmentation = segmentationService.getSegmentation(segmentationId); + + const { representationData } = segmentation; + const { volumeId, referencedVolumeId } = representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + + const labelmap = cs.cache.getVolume(volumeId); + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + // if we put it in the top, it will appear in other modes workerManager.registerWorker('suv-peak-worker', workerFn, options); - const { referencedVolumeId } = labelmap; - const referencedVolume = cs.cache.getVolume(referencedVolumeId); - - const annotationUIDs = _getAnnotationsSelectedByToolNames( - ROI_THRESHOLD_MANUAL_TOOL_IDS - ); + const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS); const annotations = annotationUIDs.map(annotationUID => csTools.annotation.state.getAnnotation(annotationUID) @@ -304,8 +309,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: origin: labelmap.origin, direction: labelmap.direction, spacing: labelmap.spacing, - scalarData: labelmap.scalarData, metadata: labelmap.metadata, + scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), }; const referenceVolumeProps = { @@ -313,8 +318,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: origin: referencedVolume.origin, direction: referencedVolume.direction, spacing: referencedVolume.spacing, - scalarData: referencedVolume.scalarData, metadata: referencedVolume.metadata, + scalarData: referencedVolume.voxelManager.getCompleteScalarDataArray(), }; // metadata in annotations has enabledElement which is not serializable @@ -335,12 +340,13 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: }; }); - const suvPeak = await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', { - labelmapProps, - referenceVolumeProps, - annotations: annotationsToSend, - segmentIndex, - }); + const suvPeak = + (await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', { + labelmapProps, + referenceVolumeProps, + annotations: annotationsToSend, + segmentIndex, + })) || {}; return { suvPeak: suvPeak.mean, @@ -349,38 +355,40 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: suvMaxLPS: suvPeak.maxLPS, }; }, - getLesionStats: ({ labelmap, segmentIndex = 1 }) => { - const { scalarData, spacing } = labelmap; - const referencedScalarData = cs.cache.getVolume(labelmap.referencedVolumeId).getScalarData(); + getLesionStats: ({ segmentationId, segmentIndex = 1 }) => { + const { labelmapVolume, referencedVolume } = getVolumesFromSegmentation(segmentationId); + const { voxelManager: segVoxelManager, imageData, spacing } = labelmapVolume; + const { voxelManager: refVoxelManager } = referencedVolume; let segmentationMax = -Infinity; let segmentationMin = Infinity; const segmentationValues = []; - let voxelCount = 0; - for (let i = 0; i < scalarData.length; i++) { - if (scalarData[i] === segmentIndex) { - const value = referencedScalarData[i]; - segmentationValues.push(value); - if (value > segmentationMax) { - segmentationMax = value; + + const callback = ({ value, index }) => { + if (value === segmentIndex) { + const refValue = refVoxelManager.getAtIndex(index) as number; + segmentationValues.push(refValue); + if (refValue > segmentationMax) { + segmentationMax = refValue; } - if (value < segmentationMin) { - segmentationMin = value; + if (refValue < segmentationMin) { + segmentationMin = refValue; } voxelCount++; } - } - const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount; + }; + segVoxelManager.forEach(callback, { imageData }); + const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount; const stats = { minValue: segmentationMin, maxValue: segmentationMax, meanValue: mean, stdValue: Math.sqrt( - segmentationValues - .map((k) => (k - mean) ** 2) - .reduce((acc, curr) => acc + curr, 0) / voxelCount), + segmentationValues.map(k => (k - mean) ** 2).reduce((acc, curr) => acc + curr, 0) / + voxelCount + ), volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3, }; @@ -393,21 +401,30 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: lesionGlyoclysisStats: volume * meanValue, }; }, - calculateTMTV: ({ segmentations }) => { - const labelmaps = segmentations.map(s => segmentationService.getLabelmapVolume(s.id)); + calculateTMTV: async ({ segmentations }) => { + const labelmapProps = segmentations.map(segmentation => { + const labelmap = getLabelmapVolumeFromSegmentation(segmentation); + return { + dimensions: labelmap.dimensions, + spacing: labelmap.spacing, + scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), + origin: labelmap.origin, + direction: labelmap.direction, + }; + }); - if (!labelmaps.length) { + if (!labelmapProps.length) { return; } - return calculateTMTV(labelmaps); + return await workerManager.executeTask('suv-peak-worker', 'calculateTMTV', labelmapProps); }, - exportTMTVReportCSV: ({ segmentations, tmtv, config, options }) => { + exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => { const segReport = commandsManager.runCommand('getSegmentationCSVReport', { segmentations, }); - const tlg = actions.getTotalLesionGlycolysis({ segmentations }); + const tlg = await actions.getTotalLesionGlycolysis({ segmentations }); const additionalReportRows = [ { key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } }, { key: 'Threshold Configuration', value: { ...config } }, @@ -422,51 +439,34 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: createAndDownloadTMTVReport(segReport, additionalReportRows, options); }, - getTotalLesionGlycolysis: ({ segmentations }) => { - const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id)); + getTotalLesionGlycolysis: async ({ segmentations }) => { + const labelmapProps = segmentations.map(segmentation => { + const labelmap = getLabelmapVolumeFromSegmentation(segmentation); + return { + dimensions: labelmap.dimensions, + spacing: labelmap.spacing, + scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), + origin: labelmap.origin, + direction: labelmap.direction, + }; + }); - let mergedLabelmap; - // merge labelmap will through an error if labels maps are not the same size - // or same direction or .... - try { - mergedLabelmap = - csTools.utilities.segmentation.createMergedLabelmapForIndex(labelmapVolumes); - } catch (e) { - console.error('commandsModule::getTotalLesionGlycolysis', e); - return; - } + const { referencedVolume: ptVolume } = getVolumesFromSegmentation( + segmentations[0].segmentationId + ); - // grabbing the first labelmap referenceVolume since it will be the same for all - const { referencedVolumeId, spacing } = labelmapVolumes[0]; + const ptVolumeProps = { + dimensions: ptVolume.dimensions, + spacing: ptVolume.spacing, + scalarData: ptVolume.voxelManager.getCompleteScalarDataArray(), + origin: ptVolume.origin, + direction: ptVolume.direction, + }; - if (!referencedVolumeId) { - console.error('commandsModule::getTotalLesionGlycolysis:No referencedVolumeId found'); - } - - const ptVolume = cs.cache.getVolume(referencedVolumeId); - const mergedLabelData = mergedLabelmap.getScalarData(); - - if (mergedLabelData.length !== ptVolume.getScalarData().length) { - console.error( - 'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size' - ); - } - - let suv = 0; - let totalLesionVoxelCount = 0; - for (let i = 0; i < mergedLabelData.length; i++) { - // if not background - if (mergedLabelData[i] !== 0) { - suv += ptVolume.getScalarData()[i]; - totalLesionVoxelCount += 1; - } - } - - // Average SUV for the merged labelmap - const averageSuv = suv / totalLesionVoxelCount; - - // total Lesion Glycolysis [suv * ml] - return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3; + return await workerManager.executeTask('suv-peak-worker', 'getTotalLesionGlycolysis', { + labelmapProps, + referenceVolumeProps: ptVolumeProps, + }); }, setStartSliceForROIThresholdTool: () => { const { viewport } = _getActiveViewportsEnabledElement(); @@ -480,7 +480,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const annotation = csTools.annotation.state.getAnnotation(annotationUID); - // set the current focalpoint + // set the current focal point annotation.data.startCoordinate = focalPoint; // IMPORTANT: invalidate the toolData for the cached stat to get updated // and re-calculate the projection points @@ -498,7 +498,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const annotation = csTools.annotation.state.getAnnotation(annotationUID); - // get the current focalpoint + // get the current focal point const focalPointToEnd = viewport.getCamera().focalPoint; annotation.data.endCoordinate = focalPointToEnd; @@ -536,27 +536,34 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const report = {}; for (const segmentation of segmentations) { - const { id, label, cachedStats: data } = segmentation; + const { label, segmentationId, representationData } = + segmentation as csTools.Types.Segmentation; + const id = segmentationId; const segReport = { id, label }; - if (!data) { + if (!representationData) { report[id] = segReport; continue; } - Object.keys(data).forEach(key => { - if (typeof data[key] !== 'object') { - segReport[key] = data[key]; - } else { - Object.keys(data[key]).forEach(subKey => { - const newKey = `${key}_${subKey}`; - segReport[newKey] = data[key][subKey]; - }); - } - }); + const { cachedStats } = segmentation.segments[1] || {}; // Assuming we want stats from the first segment - const labelmapVolume = segmentationService.getLabelmapVolume(id); + if (cachedStats) { + Object.entries(cachedStats).forEach(([key, value]) => { + if (typeof value !== 'object') { + segReport[key] = value; + } else { + Object.entries(value).forEach(([subKey, subValue]) => { + const newKey = `${key}_${subKey}`; + segReport[newKey] = subValue; + }); + } + }); + } + + const labelmapVolume = + segmentation.representationData[SegmentationRepresentations.Labelmap]; if (!labelmapVolume) { report[id] = segReport; @@ -564,9 +571,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: } const referencedVolumeId = labelmapVolume.referencedVolumeId; - segReport.referencedVolumeId = referencedVolumeId; - const referencedVolume = segmentationService.getLabelmapVolume(referencedVolumeId); + const referencedVolume = cs.cache.getVolume(referencedVolumeId); if (!referencedVolume) { report[id] = segReport; @@ -603,6 +609,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: }, setFusionPTColormap: ({ toolGroupId, colormap }) => { const toolGroup = toolGroupService.getToolGroup(toolGroupId); + + if (!toolGroup) { + return; + } + const { viewportMatchDetails } = hangingProtocolService.getMatchDetails(); const ptDisplaySet = actions.getMatchingPTDisplaySet({ @@ -650,9 +661,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: createNewLabelmapFromPT: { commandFn: actions.createNewLabelmapFromPT, }, - setSegmentationActiveForToolGroups: { - commandFn: actions.setSegmentationActiveForToolGroups, - }, thresholdSegmentationByRectangleROITool: { commandFn: actions.thresholdSegmentationByRectangleROITool, }, diff --git a/extensions/tmtv/src/getHangingProtocolModule.js b/extensions/tmtv/src/getHangingProtocolModule.ts similarity index 91% rename from extensions/tmtv/src/getHangingProtocolModule.js rename to extensions/tmtv/src/getHangingProtocolModule.ts index cfe5d65fe..d4f8d9bf6 100644 --- a/extensions/tmtv/src/getHangingProtocolModule.js +++ b/extensions/tmtv/src/getHangingProtocolModule.ts @@ -18,7 +18,7 @@ import { * image, covering all three rows. It has synchronizers for windowLevel for all CT and PT images, and * also camera synchronizer for each orientation */ -const stage1 = { +const stage1: AppTypes.HangingProtocol.ProtocolStage = { name: 'default', id: 'default', viewportStructure: { @@ -131,7 +131,7 @@ const stage2 = { * The layout follows a simple grid pattern with 2 rows and 3 columns. * It includes synchronizers as well. */ -const stage3 = { +const stage3: AppTypes.HangingProtocol.ProtocolStage = { name: '2x3-layout', id: '2x3-layout', viewportStructure: { @@ -153,7 +153,7 @@ const stage3 = { * from the top row spans the full height of both rows. * It includes synchronizers as well. */ -const stage4 = { +const stage4: AppTypes.HangingProtocol.ProtocolStage = { name: '2x4-layout', id: '2x4-layout', viewportStructure: { @@ -218,7 +218,23 @@ const stage4 = { ], }; -const ptCT = { +/** + * This layout displays three fusion viewports: axial, sagittal, and coronal. + * It follows a simple grid pattern with 1 row and 3 columns. + */ +// const stage0: AppTypes.HangingProtocol.ProtocolStage = { +// name: 'Fusion 1x3', +// viewportStructure: { +// layoutType: 'grid', +// properties: { +// rows: 1, +// columns: 3, +// }, +// }, +// viewports: [fusionAXIAL, fusionSAGITTAL, fusionCORONAL], +// }; + +const ptCT: AppTypes.HangingProtocol.Protocol = { id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', locked: true, name: 'Default', @@ -318,7 +334,6 @@ const ptCT = { ], }, }, - stages: [stage1, stage2, stage3, stage4], numberOfPriorsReferenced: -1, }; diff --git a/extensions/tmtv/src/getPanelModule.tsx b/extensions/tmtv/src/getPanelModule.tsx index 98c7b3f79..fb9cade98 100644 --- a/extensions/tmtv/src/getPanelModule.tsx +++ b/extensions/tmtv/src/getPanelModule.tsx @@ -1,14 +1,10 @@ import React from 'react'; import { PanelPetSUV, PanelROIThresholdExport } from './Panels'; import { Toolbox } from '@ohif/ui-next'; - -// TODO: -// - No loading UI exists yet -// - cancel promises when component is destroyed -// - show errors in UI for thumbnails if promise fails +import PanelTMTV from './Panels/PanelTMTV'; function getPanelModule({ commandsManager, extensionManager, servicesManager }) { - const wrappedPanelPetSuv = ({}) => { + const wrappedPanelPetSuv = () => { return ( { + const wrappedROIThresholdToolbox = () => { + return ( + + ); + }; + + const wrappedROIThresholdExport = () => { + return ( + + ); + }; + + const wrappedPanelTMTV = () => { return ( <> - - ); - }; - - const wrappedROIThresholdExport = () => { - return ( - <> + { +const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => { const imageData = vtkImageData.newInstance(); imageData.setDimensions(dimensions); imageData.setOrigin(origin); @@ -21,12 +22,21 @@ const createVolume = ({ dimensions, origin, direction, spacing, scalarData, meta imageData.modified(); + const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({ + scalarData, + dimensions, + numberOfComponents: 1, + }); return { imageData, + spacing, + origin, + direction, metadata, - getScalarData: () => scalarData, + voxelManager, }; }; + /** * This method calculates the SUV peak on a segmented ROI from a reference PET * volume. If a rectangle annotation is provided, the peak is calculated within that @@ -40,22 +50,15 @@ const createVolume = ({ dimensions, origin, direction, spacing, scalarData, meta * @returns */ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) { - const labelmap = createVolume(labelmapProps); - const referenceVolume = createVolume(referenceVolumeProps); + const labelmapInfo = createVolume(labelmapProps); + const referenceInfo = createVolume(referenceVolumeProps); - if (referenceVolume.metadata.Modality !== 'PT') { + if (referenceInfo.metadata.Modality !== 'PT') { return; } - const labelmapData = labelmap.getScalarData(); - const referenceVolumeData = referenceVolume.getScalarData(); - - if (labelmapData.length !== referenceVolumeData.length) { - throw new Error('labelmap and referenceVolume must have the same number of pixels'); - } - - const { dimensions, imageData: labelmapImageData } = labelmap; - const { imageData: referenceVolumeImageData } = referenceVolume; + const { dimensions, imageData: labelmapImageData } = labelmapInfo; + const { imageData: referenceVolumeImageData } = referenceInfo; let boundsIJK; // Todo: using the first annotation for now @@ -69,7 +72,7 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se return ijk; }); - boundsIJK = utilities.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions); + boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions); } let max = 0; @@ -77,14 +80,13 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se let maxLPS = [0, 0, 0]; const callback = ({ pointIJK, pointLPS }) => { - const offset = referenceVolumeImageData.computeOffsetIndex(pointIJK); - const value = labelmapData[offset]; + const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK); if (value !== segmentIndex) { return; } - const referenceValue = referenceVolumeData[offset]; + const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK); if (referenceValue > max) { max = referenceValue; @@ -93,7 +95,12 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se } }; - utilities.pointInShapeCallback(labelmapImageData, () => true, callback, boundsIJK); + labelmapInfo.voxelManager.forEach(callback, { + boundsIJK, + imageData: labelmapImageData, + isInObject: () => true, + returnPoints: true, + }); const direction = labelmapImageData.getDirection().slice(0, 3); @@ -123,7 +130,7 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se count += 1; }; - utilities.pointInSurroundingSphereCallback( + cstUtils.pointInSurroundingSphereCallback( referenceVolumeImageData, suvPeakCirclePoints, suvPeakMeanCallback @@ -139,8 +146,64 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se }; } +function calculateTMTV(labelmapProps, segmentIndex = 1) { + const labelmaps = labelmapProps.map(props => createVolume(props)); + + const mergedLabelmap = + labelmaps.length === 1 + ? labelmaps[0] + : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps); + + const { imageData, spacing } = mergedLabelmap; + const values = imageData.getPointData().getScalars().getData(); + + // count non-zero values inside the outputData, this would + // consider the overlapping regions to be only counted once + const numVoxels = values.reduce((acc, curr) => { + if (curr > 0) { + return acc + 1; + } + return acc; + }, 0); + + return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; +} + +function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) { + const labelmaps = labelmapProps.map(props => createVolume(props)); + + const mergedLabelmap = + labelmaps.length === 1 + ? labelmaps[0] + : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps); + + // grabbing the first labelmap referenceVolume since it will be the same for all + const { spacing } = labelmaps[0]; + + const ptVolume = createVolume(referenceVolumeProps); + + let suv = 0; + let totalLesionVoxelCount = 0; + const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength(); + for (let i = 0; i < scalarDataLength; i++) { + // if not background + if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) { + suv += ptVolume.voxelManager.getAtIndex(i); + totalLesionVoxelCount += 1; + } + } + + // Average SUV for the merged labelmap + const averageSuv = suv / totalLesionVoxelCount; + + // total Lesion Glycolysis [suv * ml] + return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3; +} + const obj = { calculateSuvPeak, + calculateTMTV, + getTotalLesionGlycolysis, }; expose(obj); diff --git a/extensions/tmtv/src/utils/calculateTMTV.ts b/extensions/tmtv/src/utils/calculateTMTV.ts index 8d9f5f3e5..0fcc680ae 100644 --- a/extensions/tmtv/src/utils/calculateTMTV.ts +++ b/extensions/tmtv/src/utils/calculateTMTV.ts @@ -20,17 +20,21 @@ function calculateTMTV(labelmaps: Array, segmentIndex = 1): volumeId ); - const { imageData, spacing } = mergedLabelmap; - const values = imageData.getPointData().getScalars().getData(); + const { imageData, spacing, voxelManager } = mergedLabelmap; // count non-zero values inside the outputData, this would // consider the overlapping regions to be only counted once - const numVoxels = values.reduce((acc, curr) => { - if (curr > 0) { - return acc + 1; + let numVoxels = 0; + const callback = ({ value }) => { + if (value > 0) { + numVoxels += 1; } - return acc; - }, 0); + }; + + voxelManager.forEach(callback, { + imageData, + isInObject: () => true, + }); return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; } diff --git a/extensions/tmtv/src/utils/handleROIThresholding.ts b/extensions/tmtv/src/utils/handleROIThresholding.ts index ab47fa032..bb5ae0c55 100644 --- a/extensions/tmtv/src/utils/handleROIThresholding.ts +++ b/extensions/tmtv/src/utils/handleROIThresholding.ts @@ -1,64 +1,97 @@ -import { cache } from '@cornerstonejs/core'; +import { Segment, Segmentation } from '@cornerstonejs/tools/types'; +import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core'; export const handleROIThresholding = async ({ segmentationId, commandsManager, segmentationService, - config = {}, -}) => { +}: withAppTypes<{ + segmentationId: string; +}>) => { const segmentation = segmentationService.getSegmentation(segmentationId); + triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { + progress: 0, + type: 'Calculate Lesion Stats', + id: segmentationId, + }); + // re-calculating the cached stats for the active segmentation const updatedPerSegmentCachedStats = {}; - segmentation.segments = await Promise.all( - segmentation.segments.map(async segment => { - if (!segment || !segment.segmentIndex) { - return segment; - } + for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) { + if (!segment) { + continue; + } - const labelmap = cache.getVolume(segmentationId); + const numericSegmentIndex = Number(segmentIndex); - const segmentIndex = segment.segmentIndex; + const lesionStats = await commandsManager.run('getLesionStats', { + segmentationId, + segmentIndex: numericSegmentIndex, + }); - const lesionStats = commandsManager.run('getLesionStats', { labelmap, segmentIndex }); - const suvPeak = await commandsManager.run('calculateSuvPeak', { labelmap, segmentIndex }); - const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue; + const suvPeak = await commandsManager.run('calculateSuvPeak', { + segmentationId, + segmentIndex: numericSegmentIndex, + }); - // update segDetails with the suv peak for the active segmentation - const cachedStats = { - lesionStats, - suvPeak, - lesionGlyoclysisStats, - }; + const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue; - segment.cachedStats = cachedStats; - segment.displayText = [ - `SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`, - `Volume: ${lesionStats.volume.toFixed(2)} mm3`, - ]; - updatedPerSegmentCachedStats[segmentIndex] = cachedStats; + // update segDetails with the suv peak for the active segmentation + const cachedStats = { + lesionStats, + suvPeak, + lesionGlyoclysisStats, + }; - return segment; - }) - ); + const updatedSegment: Segment = { + ...segment, + cachedStats: { + ...segment.cachedStats, + ...cachedStats, + }, + }; - const notYetUpdatedAtSource = true; + updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats; + segmentation.segments[segmentIndex] = updatedSegment; + } + + // all available segmentations const segmentations = segmentationService.getSegmentations(); - const tmtv = commandsManager.run('calculateTMTV', { segmentations }); + const tmtv = await commandsManager.run('calculateTMTV', { segmentations }); - segmentation.cachedStats = Object.assign(segmentation.cachedStats, updatedPerSegmentCachedStats, { - tmtv: { - value: tmtv.toFixed(3), - config: { ...config }, - }, + triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { + progress: 100, + type: 'Calculate Lesion Stats', + id: segmentationId, }); - segmentationService.addOrUpdateSegmentation( - { + // add the tmtv to all the segment cachedStats, although it is a global + // value but we don't have any other way to display it for now + // Update all segmentations with the calculated TMTV + segmentations.forEach(segmentation => { + segmentation.cachedStats = { + ...segmentation.cachedStats, + tmtv, + }; + + // Update each segment within the segmentation + Object.keys(segmentation.segments).forEach(segmentIndex => { + segmentation.segments[segmentIndex].cachedStats = { + ...segmentation.segments[segmentIndex].cachedStats, + tmtv, + }; + }); + + // Update the segmentation object + const updatedSegmentation: Segmentation = { ...segmentation, - }, - false, // don't suppress events - notYetUpdatedAtSource - ); + segments: { + ...segmentation.segments, + }, + }; + + segmentationService.addOrUpdateSegmentation(updatedSegmentation); + }); }; diff --git a/extensions/tmtv/src/utils/hpViewports.ts b/extensions/tmtv/src/utils/hpViewports.ts index cce8454cb..c206c5c52 100644 --- a/extensions/tmtv/src/utils/hpViewports.ts +++ b/extensions/tmtv/src/utils/hpViewports.ts @@ -1,4 +1,22 @@ -const ctAXIAL = { +// Common sync group configurations +const cameraPositionSync = (id: string) => ({ + type: 'cameraPosition', + id, + source: true, + target: true, +}); + +const hydrateSegSync = { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, +}; + +const ctAXIAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'ctAXIAL', viewportType: 'volume', @@ -9,12 +27,7 @@ const ctAXIAL = { preset: 'first', // 'first', 'last', 'middle' }, syncGroups: [ - { - type: 'cameraPosition', - id: 'axialSync', - source: true, - target: true, - }, + cameraPositionSync('axialSync'), { type: 'voi', id: 'ctWLSync', @@ -24,6 +37,7 @@ const ctAXIAL = { syncColormap: true, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -33,49 +47,14 @@ const ctAXIAL = { ], }; -const ctSAGITTAL = { +const ctSAGITTAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'ctSAGITTAL', viewportType: 'volume', orientation: 'sagittal', toolGroupId: 'ctToolGroup', syncGroups: [ - { - type: 'cameraPosition', - id: 'sagittalSync', - source: true, - target: true, - }, - { - type: 'voi', - id: 'ctWLSync', - source: true, - target: true, - options: { - syncColormap: true, - }, - }, - ], - }, - displaySets: [ - { - id: 'ctDisplaySet', - }, - ], -}; -const ctCORONAL = { - viewportOptions: { - viewportId: 'ctCORONAL', - viewportType: 'volume', - orientation: 'coronal', - toolGroupId: 'ctToolGroup', - syncGroups: [ - { - type: 'cameraPosition', - id: 'coronalSync', - source: true, - target: true, - }, + cameraPositionSync('sagittalSync'), { type: 'voi', id: 'ctWLSync', @@ -85,6 +64,7 @@ const ctCORONAL = { syncColormap: true, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -94,7 +74,34 @@ const ctCORONAL = { ], }; -const ptAXIAL = { +const ctCORONAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ctCORONAL', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: 'ctToolGroup', + syncGroups: [ + cameraPositionSync('coronalSync'), + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], +}; + +const ptAXIAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'ptAXIAL', viewportType: 'volume', @@ -106,12 +113,7 @@ const ptAXIAL = { preset: 'first', // 'first', 'last', 'middle' }, syncGroups: [ - { - type: 'cameraPosition', - id: 'axialSync', - source: true, - target: true, - }, + cameraPositionSync('axialSync'), { type: 'voi', id: 'ptWLSync', @@ -131,6 +133,7 @@ const ptAXIAL = { syncInvertState: false, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -146,7 +149,7 @@ const ptAXIAL = { ], }; -const ptSAGITTAL = { +const ptSAGITTAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'ptSAGITTAL', viewportType: 'volume', @@ -154,12 +157,7 @@ const ptSAGITTAL = { background: [1, 1, 1], toolGroupId: 'ptToolGroup', syncGroups: [ - { - type: 'cameraPosition', - id: 'sagittalSync', - source: true, - target: true, - }, + cameraPositionSync('sagittalSync'), { type: 'voi', id: 'ptWLSync', @@ -179,6 +177,7 @@ const ptSAGITTAL = { syncInvertState: false, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -194,7 +193,7 @@ const ptSAGITTAL = { ], }; -const ptCORONAL = { +const ptCORONAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'ptCORONAL', viewportType: 'volume', @@ -202,12 +201,7 @@ const ptCORONAL = { background: [1, 1, 1], toolGroupId: 'ptToolGroup', syncGroups: [ - { - type: 'cameraPosition', - id: 'coronalSync', - source: true, - target: true, - }, + cameraPositionSync('coronalSync'), { type: 'voi', id: 'ptWLSync', @@ -227,6 +221,7 @@ const ptCORONAL = { syncInvertState: false, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -242,7 +237,7 @@ const ptCORONAL = { ], }; -const fusionAXIAL = { +const fusionAXIAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'fusionAXIAL', viewportType: 'volume', @@ -253,12 +248,7 @@ const fusionAXIAL = { preset: 'first', // 'first', 'last', 'middle' }, syncGroups: [ - { - type: 'cameraPosition', - id: 'axialSync', - source: true, - target: true, - }, + cameraPositionSync('axialSync'), { type: 'voi', id: 'ctWLSync', @@ -284,6 +274,7 @@ const fusionAXIAL = { syncInvertState: false, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -297,8 +288,8 @@ const fusionAXIAL = { name: 'hsv', opacity: [ { value: 0, opacity: 0 }, - { value: 0.1, opacity: 0.9 }, - { value: 1, opacity: 0.95 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.9 }, ], }, voi: { @@ -320,12 +311,7 @@ const fusionSAGITTAL = { // preset: 'middle', // 'first', 'last', 'middle' // }, syncGroups: [ - { - type: 'cameraPosition', - id: 'sagittalSync', - source: true, - target: true, - }, + cameraPositionSync('sagittalSync'), { type: 'voi', id: 'ctWLSync', @@ -351,6 +337,7 @@ const fusionSAGITTAL = { syncInvertState: false, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -364,8 +351,8 @@ const fusionSAGITTAL = { name: 'hsv', opacity: [ { value: 0, opacity: 0 }, - { value: 0.1, opacity: 0.9 }, - { value: 1, opacity: 0.95 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.9 }, ], }, voi: { @@ -387,12 +374,7 @@ const fusionCORONAL = { // preset: 'middle', // 'first', 'last', 'middle' // }, syncGroups: [ - { - type: 'cameraPosition', - id: 'coronalSync', - source: true, - target: true, - }, + cameraPositionSync('coronalSync'), { type: 'voi', id: 'ctWLSync', @@ -418,6 +400,7 @@ const fusionCORONAL = { syncInvertState: false, }, }, + hydrateSegSync, ], }, displaySets: [ @@ -431,8 +414,8 @@ const fusionCORONAL = { name: 'hsv', opacity: [ { value: 0, opacity: 0 }, - { value: 0.1, opacity: 0.9 }, - { value: 1, opacity: 0.95 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.9 }, ], }, voi: { @@ -443,7 +426,7 @@ const fusionCORONAL = { ], }; -const mipSAGITTAL = { +const mipSAGITTAL: AppTypes.HangingProtocol.Viewport = { viewportOptions: { viewportId: 'mipSagittal', viewportType: 'volume', @@ -470,6 +453,7 @@ const mipSAGITTAL = { syncInvertState: false, }, }, + hydrateSegSync, ], // Custom props can be used to set custom properties which extensions diff --git a/increaseEventEmitterLimit.mjs b/increaseEventEmitterLimit.mjs deleted file mode 100644 index 22ff97d37..000000000 --- a/increaseEventEmitterLimit.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// increase the event emitter limit -import { EventEmitter } from 'events'; - -EventEmitter.defaultMaxListeners = 1000; - -// process -process.setMaxListeners(1000); diff --git a/modes/basic-dev-mode/src/index.ts b/modes/basic-dev-mode/src/index.ts index 649dd81cd..48378982d 100644 --- a/modes/basic-dev-mode/src/index.ts +++ b/modes/basic-dev-mode/src/index.ts @@ -72,7 +72,10 @@ function modeFactory({ modeConfiguration }) { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts index 350a51b1f..740ff9079 100644 --- a/modes/basic-test-mode/src/index.ts +++ b/modes/basic-test-mode/src/index.ts @@ -42,7 +42,10 @@ const dicompdf = { const dicomSeg = { sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', - panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', +}; + +const cornerstone = { + panel: '@ohif/extension-cornerstone.panelModule.panelSegmentation', }; const dicomPmap = { @@ -147,7 +150,7 @@ function modeFactory() { // leftPanels: [ohif.thumbnailList], // rightPanels: [dicomSeg.panel, ohif.measurements], leftPanels: [tracked.thumbnailList], - rightPanels: [dicomSeg.panel, tracked.measurements], + rightPanels: [cornerstone.panel, tracked.measurements], // rightPanelClosed: true, // optional prop to start with collapse panels viewports: [ { diff --git a/modes/basic-test-mode/src/initToolGroups.ts b/modes/basic-test-mode/src/initToolGroups.ts index b313d8e0b..ef56f9cd6 100644 --- a/modes/basic-test-mode/src/initToolGroups.ts +++ b/modes/basic-test-mode/src/initToolGroups.ts @@ -31,7 +31,10 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, @@ -62,7 +65,6 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage { toolName: toolNames.Angle }, { toolName: toolNames.CobbAngle }, { toolName: toolNames.Magnify }, - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.WindowLevelRegion }, { toolName: toolNames.UltrasoundDirectional }, { toolName: toolNames.PlanarFreehandROI }, @@ -116,8 +118,8 @@ function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { ], }, { - toolName: toolNames.StackScrollMouseWheel, - bindings: [], + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], }, ], passive: [ @@ -165,7 +167,10 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, @@ -194,7 +199,6 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { { toolName: toolNames.RectangleROI }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Angle }, - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.WindowLevelRegion }, { toolName: toolNames.PlanarFreehandROI }, { toolName: toolNames.SplineROI }, diff --git a/modes/basic-test-mode/src/moreTools.ts b/modes/basic-test-mode/src/moreTools.ts index 204bac680..91634d0f0 100644 --- a/modes/basic-test-mode/src/moreTools.ts +++ b/modes/basic-test-mode/src/moreTools.ts @@ -68,7 +68,7 @@ const moreTools = [ }, }, listeners: { - [EVENTS.STACK_VIEWPORT_NEW_STACK]: { + [EVENTS.VIEWPORT_NEW_IMAGE_SET]: { commandName: 'toggleImageSliceSync', commandOptions: { toggledState: true }, }, @@ -188,7 +188,13 @@ const moreTools = [ label: 'Ultrasound Directional', tooltip: 'Ultrasound Directional', commands: setToolActiveToolbar, - evaluate: ['evaluate.cornerstoneTool', 'evaluate.isUS'], + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + }, + ], }), createButton({ id: 'WindowLevelRegion', diff --git a/modes/longitudinal/src/customizations.tsx b/modes/longitudinal/src/customizations.tsx new file mode 100644 index 000000000..649a8bd46 --- /dev/null +++ b/modes/longitudinal/src/customizations.tsx @@ -0,0 +1,26 @@ +export const performCustomizations = customizationService => { + // Set the custom SegmentationTable + customizationService.addModeCustomizations([ + // To disable editing in the SegmentationTable + { + id: 'PanelSegmentation.disableEditing', + disableEditing: true, + }, + // To disable editing in the MeasurementTable + // { + // id: 'PanelMeasurement.disableEditing', + // disableEditing: true, + // }, + // { + // id: 'measurementLabels', + // labelOnMeasure: true, + // exclusive: true, + // items: [ + // { value: 'Head', label: 'Head' }, + // { value: 'Shoulder', label: 'Shoulder' }, + // { value: 'Knee', label: 'Knee' }, + // { value: 'Toe', label: 'Toe' }, + // ], + // }, + ]); +}; diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts index fa4b90e9f..7ce082c77 100644 --- a/modes/longitudinal/src/index.ts +++ b/modes/longitudinal/src/index.ts @@ -4,6 +4,7 @@ import { id } from './id'; import initToolGroups from './initToolGroups'; import toolbarButtons from './toolbarButtons'; import moreTools from './moreTools'; +import { performCustomizations } from './customizations'; // Allow this mode by excluding non-imaging modalities such as SR, SEG // Also, SM is not a simple imaging modalities, so exclude it. @@ -15,7 +16,11 @@ const ohif = { thumbnailList: '@ohif/extension-default.panelModule.seriesList', wsiSopClassHandler: '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', - measurements: '@ohif/extension-default.panelModule.measurements', +}; + +const cornerstone = { + measurements: '@ohif/extension-cornerstone.panelModule.measurements', + segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation', }; const tracked = { @@ -43,7 +48,6 @@ const dicompdf = { const dicomSeg = { sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', - panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', }; const dicomPmap = { @@ -86,19 +90,7 @@ function modeFactory({ modeConfiguration }) { measurementService.clearMeasurements(); - // customizationService.addModeCustomizations([ - // { - // id: 'measurementLabels', - // labelOnMeasure: true, - // exclusive: true, - // items: [ - // { value: 'Head', label: 'Head' }, - // { value: 'Shoulder', label: 'Shoulder' }, - // { value: 'Knee', label: 'Knee' }, - // { value: 'Toe', label: 'Toe' }, - // ], - // }, - // ]); + performCustomizations(customizationService); // Init Default and SR ToolGroups initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig); @@ -116,13 +108,6 @@ function modeFactory({ modeConfiguration }) { 'MoreTools', ]); - customizationService.addModeCustomizations([ - { - id: 'segmentation.panel', - disableEditing: true, - }, - ]); - // // ActivatePanel event trigger for when a segmentation or measurement is added. // // Do not force activation so as to respect the state the user may have left the UI in. // _activatePanelTriggersSubscriptions = [ @@ -192,7 +177,7 @@ function modeFactory({ modeConfiguration }) { id: ohif.layout, props: { leftPanels: [tracked.thumbnailList], - rightPanels: [dicomSeg.panel, tracked.measurements], + rightPanels: [cornerstone.segmentation, tracked.measurements], rightPanelClosed: true, viewports: [ { diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index 20d1cd622..6743602d0 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -39,7 +39,10 @@ function initDefaultToolGroup( toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, @@ -77,7 +80,6 @@ function initDefaultToolGroup( { toolName: toolNames.Angle }, { toolName: toolNames.CobbAngle }, { toolName: toolNames.Magnify }, - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.CalibrationLine }, { toolName: toolNames.PlanarFreehandContourSegmentation, @@ -150,8 +152,8 @@ function initSRToolGroup(extensionManager, toolGroupService) { ], }, { - toolName: toolNames.StackScrollMouseWheel, - bindings: [], + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], }, ], passive: [ @@ -163,12 +165,6 @@ function initSRToolGroup(extensionManager, toolGroupService) { { toolName: SRToolNames.SRPlanarFreehandROI }, { toolName: SRToolNames.SRRectangleROI }, { toolName: toolNames.WindowLevelRegion }, - { - toolName: SRToolNames.SRPlanarFreehandContourSegmentation, - configuration: { - displayOnePointAsCrosshairs: true, - }, - }, ], enabled: [ { @@ -206,7 +202,10 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager, m toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, @@ -244,7 +243,6 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager, m { toolName: toolNames.Angle }, { toolName: toolNames.CobbAngle }, { toolName: toolNames.PlanarFreehandROI }, - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.WindowLevelRegion }, { toolName: toolNames.PlanarFreehandContourSegmentation, diff --git a/modes/longitudinal/src/moreTools.ts b/modes/longitudinal/src/moreTools.ts index 3c4ca9310..483cc0aa5 100644 --- a/modes/longitudinal/src/moreTools.ts +++ b/modes/longitudinal/src/moreTools.ts @@ -46,7 +46,13 @@ const moreTools = [ label: 'Rotate Right', tooltip: 'Rotate +90', commands: 'rotateViewportCW', - evaluate: 'evaluate.action', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'flipHorizontal', @@ -54,7 +60,13 @@ const moreTools = [ label: 'Flip Horizontal', tooltip: 'Flip Horizontally', commands: 'flipViewportHorizontal', - evaluate: ['evaluate.viewportProperties.toggle', 'evaluate.not3D'], + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'volume3d'], + }, + ], }), createButton({ id: 'ImageSliceSync', @@ -68,12 +80,18 @@ const moreTools = [ }, }, listeners: { - [EVENTS.STACK_VIEWPORT_NEW_STACK]: { + [EVENTS.VIEWPORT_NEW_IMAGE_SET]: { commandName: 'toggleImageSliceSync', commandOptions: { toggledState: true }, }, }, - evaluate: ['evaluate.cornerstone.synchronizer', 'evaluate.not3D'], + evaluate: [ + 'evaluate.cornerstone.synchronizer', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'volume3d'], + }, + ], }), createButton({ id: 'ReferenceLines', @@ -85,7 +103,13 @@ const moreTools = [ [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, }, - evaluate: 'evaluate.cornerstoneTool.toggle', + evaluate: [ + 'evaluate.cornerstoneTool.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'ImageOverlayViewer', @@ -93,7 +117,13 @@ const moreTools = [ label: 'Image Overlay', tooltip: 'Toggle Image Overlay', commands: 'toggleEnabledDisabledToolbar', - evaluate: 'evaluate.cornerstoneTool.toggle', + evaluate: [ + 'evaluate.cornerstoneTool.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'StackScroll', @@ -109,7 +139,13 @@ const moreTools = [ label: 'Invert', tooltip: 'Invert Colors', commands: 'invertViewport', - evaluate: 'evaluate.viewportProperties.toggle', + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'Probe', @@ -125,7 +161,13 @@ const moreTools = [ label: 'Cine', tooltip: 'Cine', commands: 'toggleCine', - evaluate: ['evaluate.cine', 'evaluate.not3D'], + evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], }), createButton({ id: 'Angle', @@ -149,7 +191,13 @@ const moreTools = [ label: 'Zoom-in', tooltip: 'Zoom-in', commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'CalibrationLine', @@ -157,7 +205,13 @@ const moreTools = [ label: 'Calibration', tooltip: 'Calibration Line', commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'TagBrowser', @@ -172,7 +226,13 @@ const moreTools = [ label: 'Magnify Probe', tooltip: 'Magnify Probe', commands: 'toggleActiveDisabledToolbar', - evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + evaluate: [ + 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), createButton({ id: 'UltrasoundDirectionalTool', @@ -180,7 +240,13 @@ const moreTools = [ label: 'Ultrasound Directional', tooltip: 'Ultrasound Directional', commands: setToolActiveToolbar, - evaluate: ['evaluate.cornerstoneTool', 'evaluate.isUS'], + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + }, + ], }), createButton({ id: 'WindowLevelRegion', @@ -188,7 +254,13 @@ const moreTools = [ label: 'Window Level Region', tooltip: 'Window Level Region', commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }), ], }, diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts index 9b98db3fa..145e98439 100644 --- a/modes/longitudinal/src/toolbarButtons.ts +++ b/modes/longitudinal/src/toolbarButtons.ts @@ -126,7 +126,13 @@ const toolbarButtons: Button[] = [ icon: 'tool-window-level', label: 'Window Level', commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['wholeSlide'], + }, + ], }, }, // Pan... @@ -162,7 +168,13 @@ const toolbarButtons: Button[] = [ icon: 'tool-capture', label: 'Capture', commands: 'showDownloadViewportModal', - evaluate: 'evaluate.action', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], }, }, { diff --git a/modes/preclinical-4d/src/getWorkflowSettings.ts b/modes/preclinical-4d/src/getWorkflowSettings.ts index efaab9b1f..624175ff5 100644 --- a/modes/preclinical-4d/src/getWorkflowSettings.ts +++ b/modes/preclinical-4d/src/getWorkflowSettings.ts @@ -2,12 +2,11 @@ const dynamicVolume = { sopClassHandler: '@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume', leftPanel: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume', - toolBox: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox', - export: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export', + segmentation: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-segmentation', }; const cornerstone = { - segmentation: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', + segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader', activeViewportWindowLevel: '@ohif/extension-cornerstone.panelModule.activeViewportWindowLevel', }; @@ -60,7 +59,7 @@ function getWorkflowSettings({ servicesManager }) { layout: { panels: { left: defaultLeftPanel, - right: [[dynamicVolume.toolBox, cornerstone.segmentation, dynamicVolume.export]], + right: [[dynamicVolume.segmentation]], }, options: { leftPanelClosed: false, diff --git a/modes/preclinical-4d/src/index.tsx b/modes/preclinical-4d/src/index.tsx index 209e30fc0..23ac99294 100644 --- a/modes/preclinical-4d/src/index.tsx +++ b/modes/preclinical-4d/src/index.tsx @@ -36,7 +36,7 @@ function modeFactory({ modeConfiguration }) { return { id, routeName: 'dynamic-volume', - displayName: '4D PT/CT', + displayName: 'Preclinical 4D', onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) { const { measurementService, @@ -64,11 +64,31 @@ function modeFactory({ modeConfiguration }) { // specific to the step customizationService.addModeCustomizations([ { - id: 'segmentation.panel', - segmentationPanelMode: 'expanded', - addSegment: false, + id: 'PanelSegmentation.tableMode', + mode: 'expanded', + }, + { + id: 'PanelSegmentation.onSegmentationAdd', onSegmentationAdd: () => { - commandsManager.run('createNewLabelmapFromPT'); + commandsManager.run('createNewLabelMapForDynamicVolume'); + }, + }, + { + id: 'PanelSegmentation.showAddSegment', + showAddSegment: false, + }, + { + id: 'PanelSegmentation.readableText', + // remove following if you are not interested in that stats + readableText: { + lesionStats: 'Lesion Statistics', + minValue: 'Minimum Value', + maxValue: 'Maximum Value', + meanValue: 'Mean Value', + volume: 'Volume', + suvPeak: 'SUV Peak', + suvMax: 'Maximum SUV', + suvMaxIJK: 'SUV Max IJK', }, }, ]); diff --git a/modes/preclinical-4d/src/initToolGroups.tsx b/modes/preclinical-4d/src/initToolGroups.tsx index e477c7feb..6f8dd0ef8 100644 --- a/modes/preclinical-4d/src/initToolGroups.tsx +++ b/modes/preclinical-4d/src/initToolGroups.tsx @@ -33,7 +33,10 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, se toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, @@ -100,9 +103,8 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, se { toolName: toolNames.SphereScissors }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Magnify }, - { toolName: toolNames.SegmentationDisplay }, ], - enabled: [{ toolName: toolNames.SegmentationDisplay }], + enabled: [], disabled: [ { toolName: toolNames.Crosshairs, diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index a37e61763..2a53e5998 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -4,9 +4,6 @@ import toolbarButtons from './toolbarButtons'; import segmentationButtons from './segmentationButtons'; import initToolGroups from './initToolGroups'; -const DEFAULT_TOOL_GROUP_ID = 'default'; -const VOLUME3D_TOOL_GROUP_ID = 'volume3d'; - const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', @@ -17,11 +14,10 @@ const ohif = { const cornerstone = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', + panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools', }; const segmentation = { - panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', - panelTool: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationWithTools', sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', }; @@ -80,7 +76,6 @@ function modeFactory({ modeConfiguration }) { const { toolGroupService, syncGroupService, - toolbarService, segmentationService, cornerstoneViewportService, uiDialogService, @@ -111,10 +106,10 @@ function modeFactory({ modeConfiguration }) { return { valid: modalitiesArray.length === 1 - ? !['SM', 'US', 'MG', 'OT', 'DOC', 'CR'].includes(modalitiesArray[0]) + ? !['SM', 'ECG', 'OT', 'DOC'].includes(modalitiesArray[0]) : true, description: - 'The mode does not support studies that ONLY include the following modalities: SM, US, MG, OT, DOC, CR', + 'The mode does not support studies that ONLY include the following modalities: SM, OT, DOC', }; }, /** @@ -137,7 +132,8 @@ function modeFactory({ modeConfiguration }) { id: ohif.layout, props: { leftPanels: [ohif.leftPanel], - rightPanels: [segmentation.panelTool], + rightPanels: [cornerstone.panelTool], + leftPanelClosed: true, viewports: [ { namespace: cornerstone.viewport, diff --git a/modes/segmentation/src/initToolGroups.ts b/modes/segmentation/src/initToolGroups.ts index babfe4a11..387a4b0da 100644 --- a/modes/segmentation/src/initToolGroups.ts +++ b/modes/segmentation/src/initToolGroups.ts @@ -17,7 +17,7 @@ function createTools(utilityModule) { { toolName: toolNames.WindowLevel, bindings: [{ mouseButton: Enums.MouseBindings.Primary }] }, { toolName: toolNames.Pan, bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }] }, { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }] }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { toolName: toolNames.StackScroll, bindings: [{ mouseButton: Enums.MouseBindings.Wheel }] }, ], passive: [ { @@ -86,7 +86,6 @@ function createTools(utilityModule) { { toolName: toolNames.SphereScissors }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Magnify }, - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.WindowLevelRegion }, { toolName: toolNames.UltrasoundDirectional }, diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts index 89b4092c6..91fc11e20 100644 --- a/modes/segmentation/src/toolbarButtons.ts +++ b/modes/segmentation/src/toolbarButtons.ts @@ -142,7 +142,13 @@ const toolbarButtons: Button[] = [ label: 'Flip Horizontal', tooltip: 'Flip Horizontally', commands: 'flipViewportHorizontal', - evaluate: ['evaluate.viewportProperties.toggle', 'evaluate.not3D'], + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], }), createButton({ id: 'ReferenceLines', @@ -194,7 +200,13 @@ const toolbarButtons: Button[] = [ label: 'Cine', tooltip: 'Cine', commands: 'toggleCine', - evaluate: ['evaluate.cine', 'evaluate.not3D'], + evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], }), createButton({ id: 'Angle', @@ -249,7 +261,13 @@ const toolbarButtons: Button[] = [ label: 'Ultrasound Directional', tooltip: 'Ultrasound Directional', commands: setToolActiveToolbar, - evaluate: ['evaluate.cornerstoneTool', 'evaluate.isUS'], + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + }, + ], }), createButton({ id: 'WindowLevelRegion', diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts index 1ef04ca73..ef383e65d 100644 --- a/modes/tmtv/src/index.ts +++ b/modes/tmtv/src/index.ts @@ -11,20 +11,19 @@ const { MetadataProvider } = classes; const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', - measurements: '@ohif/extension-default.panelModule.measure', thumbnailList: '@ohif/extension-default.panelModule.seriesList', }; const cs3d = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', - segPanel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', + segPanel: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader', + measurements: '@ohif/extension-cornerstone.panelModule.measurements', }; const tmtv = { hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', petSUV: '@ohif/extension-tmtv.panelModule.petSUV', - toolbox: '@ohif/extension-tmtv.panelModule.tmtvBox', - export: '@ohif/extension-tmtv.panelModule.tmtvExport', + tmtv: '@ohif/extension-tmtv.panelModule.tmtv', }; const extensionDependencies = { @@ -104,13 +103,30 @@ function modeFactory({ modeConfiguration }) { customizationService.addModeCustomizations([ { - id: 'segmentation.panel', - segmentationPanelMode: 'expanded', - addSegment: false, + id: 'PanelSegmentation.tableMode', + mode: 'expanded', + }, + { + id: 'PanelSegmentation.onSegmentationAdd', onSegmentationAdd: () => { commandsManager.run('createNewLabelmapFromPT'); }, }, + { + id: 'PanelSegmentation.readableText', + // remove following if you are not interested in that stats + readableText: { + lesionStats: 'Lesion Statistics', + minValue: 'Minimum Value', + maxValue: 'Maximum Value', + meanValue: 'Mean Value', + volume: 'Volume', + suvPeak: 'SUV Peak', + suvMax: 'Maximum SUV', + suvMaxIJK: 'SUV Max IJK', + lesionGlyoclysisStats: 'Lesion Glycolysis', + }, + }, ]); // For the hanging protocol we need to decide on the window level @@ -200,7 +216,7 @@ function modeFactory({ modeConfiguration }) { props: { leftPanels: [ohif.thumbnailList], leftPanelClosed: true, - rightPanels: [[tmtv.toolbox, cs3d.segPanel, tmtv.export], tmtv.petSUV], + rightPanels: [tmtv.tmtv, tmtv.petSUV], viewports: [ { namespace: cs3d.viewport, diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js index 899bcc32d..73f66eb62 100644 --- a/modes/tmtv/src/initToolGroups.js +++ b/modes/tmtv/src/initToolGroups.js @@ -6,14 +6,7 @@ export const toolGroupIds = { default: 'default', }; -function _initToolGroups( - toolNames, - Enums, - toolGroupService, - commandsManager, - modeLabelConfig, - servicesManager -) { +function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig) { const tools = { active: [ { @@ -28,7 +21,10 @@ function _initToolGroups( toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, ], passive: [ { toolName: toolNames.Length }, @@ -127,7 +123,7 @@ function _initToolGroups( }, }, ], - enabled: [{ toolName: toolNames.SegmentationDisplay }], + enabled: [], disabled: [ { toolName: toolNames.Crosshairs, @@ -155,7 +151,8 @@ function _initToolGroups( const mipTools = { active: [ { - toolName: toolNames.VolumeRotateMouseWheel, + toolName: toolNames.VolumeRotate, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], configuration: { rotateIncrementDegrees: 5, }, @@ -169,7 +166,6 @@ function _initToolGroups( }, ], enabled: [ - { toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.OrientationMarker, configuration: { @@ -184,22 +180,8 @@ function _initToolGroups( toolGroupService.createToolGroupAndAddTools(toolGroupIds.MIP, mipTools); } -function initToolGroups( - toolNames, - Enums, - toolGroupService, - commandsManager, - modeLabelConfig, - servicesManager -) { - _initToolGroups( - toolNames, - Enums, - toolGroupService, - commandsManager, - modeLabelConfig, - servicesManager - ); +function initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig) { + _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig); } export default initToolGroups; diff --git a/modes/tmtv/src/utils/setFusionActiveVolume.js b/modes/tmtv/src/utils/setFusionActiveVolume.js index 30d42455c..75f4538dc 100644 --- a/modes/tmtv/src/utils/setFusionActiveVolume.js +++ b/modes/tmtv/src/utils/setFusionActiveVolume.js @@ -7,6 +7,7 @@ export default function setFusionActiveVolume( displaySetService ) { const matchDetails = matches.get('ptDisplaySet'); + const matchDetails2 = matches.get('ctDisplaySet'); if (!matchDetails) { return; @@ -32,10 +33,13 @@ export default function setFusionActiveVolume( // Todo: this should not take into account the loader id const volumeId = `cornerstoneStreamingImageVolume:${displaySets[0].displaySetInstanceUID}`; + const { SeriesInstanceUID: SeriesInstanceUID2 } = matchDetails2; + const ctDisplaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID2); + const ctVolumeId = `cornerstoneStreamingImageVolume:${ctDisplaySets[0].displaySetInstanceUID}`; const windowLevelConfig = { ...wlToolConfig, - volumeId, + volumeId: ctVolumeId, }; const ellipticalROIConfig = { diff --git a/package.json b/package.json index 497cb13bb..3f54ce3c9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dev:orthanc:no:cache": "lerna run dev:orthanc:no:cache --stream", "dev:dcm4chee": "lerna run dev:dcm4chee --stream", "dev:static": "lerna run dev:static --stream", - "orthanc:up": "docker-compose -f platform/app/.recipes/Nginx-Orthanc/docker-compose.yml up", + "orthanc:up": "docker compose -f platform/app/.recipes/Nginx-Orthanc/docker-compose.yml up", "install:dev": "cp -f yarn.lock addOns/yarn.lock && cd addOns && yarn install --modules-folder ../node_modules", "preinstall": "node preinstall.js", "start": "yarn run dev", @@ -86,8 +86,8 @@ "@babel/preset-env": "7.24.7", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.13.0", - "prettier": "^3.3.2", - "prettier-plugin-tailwindcss": "^0.6.5" + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "0.6.8" }, "husky": { "hooks": { @@ -110,7 +110,7 @@ "glob-parent": "^6.0.2", "trim": "^1.0.0", "package-json": "^8.1.0", - "typescript": "4.6.4", + "typescript": "5.5.4", "sharp": "^0.32.6", "ip": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "**/express/path-to-regexp": "0.1.10", diff --git a/platform/app/.gitignore b/platform/app/.gitignore index 82f6c1b36..9062b1c28 100644 --- a/platform/app/.gitignore +++ b/platform/app/.gitignore @@ -1,2 +1,3 @@ .vercel test-results +store.json diff --git a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile index fa066291e..6c5e0b873 100644 --- a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile +++ b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile @@ -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 diff --git a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile index 643c52a57..53a0087ff 100644 --- a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile +++ b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile @@ -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 diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js index 6a90e73b4..cd6487589 100644 --- a/platform/app/.webpack/webpack.pwa.js +++ b/platform/app/.webpack/webpack.pwa.js @@ -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) { diff --git a/platform/app/cypress.config.ts b/platform/app/cypress.config.ts index debb68a6f..eecb13a79 100644 --- a/platform/app/cypress.config.ts +++ b/platform/app/cypress.config.ts @@ -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; diff --git a/platform/app/cypress/integration/customization/HangingProtocol.spec.js b/platform/app/cypress/integration/customization/HangingProtocol.spec.js index 743caecba..795db727a 100644 --- a/platform/app/cypress/integration/customization/HangingProtocol.spec.js +++ b/platform/app/cypress/integration/customization/HangingProtocol.spec.js @@ -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', () => { diff --git a/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js b/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js index 431ff8b4e..20a3a6cd1 100644 --- a/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js +++ b/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js @@ -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. diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js index 7e6806413..d307b5139 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js @@ -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'); }); }); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js index 444ed5a09..bf551565d 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js @@ -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); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js index bb5c1716e..1cd9fa334 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js @@ -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/'); diff --git a/platform/app/netlify.toml b/platform/app/netlify.toml index bc07d914e..9e4a6e5ee 100644 --- a/platform/app/netlify.toml +++ b/platform/app/netlify.toml @@ -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" diff --git a/platform/app/package.json b/platform/app/package.json index 289278331..aa31dbecf 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -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", diff --git a/platform/app/public/_headers b/platform/app/public/_headers index dfe145ba7..e69de29bb 100644 --- a/platform/app/public/_headers +++ b/platform/app/public/_headers @@ -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 diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 61923e5da..0e5443099 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -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', diff --git a/platform/app/public/config/default_16bit.js b/platform/app/public/config/default_16bit.js index 4c11977cc..d1b6558ca 100644 --- a/platform/app/public/config/default_16bit.js +++ b/platform/app/public/config/default_16bit.js @@ -17,7 +17,6 @@ window.config = { showCPUFallbackMessage: true, showLoadingIndicator: true, useNorm16Texture: true, - useSharedArrayBuffer: 'AUTO', maxNumRequests: { interaction: 100, thumbnail: 75, diff --git a/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js b/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js index 163703b7a..b5ea6c45b 100644 --- a/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js +++ b/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js @@ -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 diff --git a/platform/app/public/config/docker-nginx-dcm4chee.js b/platform/app/public/config/docker-nginx-dcm4chee.js index 6d7e9fc47..b44f35788 100644 --- a/platform/app/public/config/docker-nginx-dcm4chee.js +++ b/platform/app/public/config/docker-nginx-dcm4chee.js @@ -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 diff --git a/platform/app/public/config/docker-nginx-orthanc.js b/platform/app/public/config/docker-nginx-orthanc.js index 7a480ea89..f4be9f944 100644 --- a/platform/app/public/config/docker-nginx-orthanc.js +++ b/platform/app/public/config/docker-nginx-orthanc.js @@ -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 diff --git a/platform/app/public/config/google.js b/platform/app/public/config/google.js index e416a9e81..08799ff84 100644 --- a/platform/app/public/config/google.js +++ b/platform/app/public/config/google.js @@ -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, diff --git a/platform/app/public/config/local_dcm4chee.js b/platform/app/public/config/local_dcm4chee.js index 5148bf4a4..1e223a4b9 100644 --- a/platform/app/public/config/local_dcm4chee.js +++ b/platform/app/public/config/local_dcm4chee.js @@ -1,10 +1,6 @@ /** @type {AppTypes.Config} */ window.config = { routerBasename: '/', - customizationService: { - dicomUploadComponent: - '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', - }, showStudyList: true, extensions: [], modes: [], diff --git a/platform/app/public/config/local_orthanc.js b/platform/app/public/config/local_orthanc.js index 9fa52fe6b..b5c287f5f 100644 --- a/platform/app/public/config/local_orthanc.js +++ b/platform/app/public/config/local_orthanc.js @@ -3,10 +3,6 @@ window.config = { routerBasename: '/', extensions: [], modes: [], - customizationService: { - dicomUploadComponent: - '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', - }, showStudyList: true, maxNumberOfWebWorkers: 3, showLoadingIndicator: true, diff --git a/platform/app/public/config/netlify.js b/platform/app/public/config/netlify.js index 65857e0da..678b88a6c 100644 --- a/platform/app/public/config/netlify.js +++ b/platform/app/public/config/netlify.js @@ -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', diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 23690d612..253080bd7 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -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], diff --git a/platform/app/src/appInit.js b/platform/app/src/appInit.js index 60f5ee4db..ce864b054 100644 --- a/platform/app/src/appInit.js +++ b/platform/app/src/appInit.js @@ -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], ]); diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index f520e6cd7..0d41892e3 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -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, }); } diff --git a/platform/app/src/routes/Debug.tsx b/platform/app/src/routes/Debug.tsx index 0bebe7f60..67ad96544 100644 --- a/platform/app/src/routes/Debug.tsx +++ b/platform/app/src/routes/Debug.tsx @@ -6,7 +6,7 @@ import { Icon } from '@ohif/ui'; function Debug() { return (
-
+
- {!window.crossOriginIsolated && ( -
- 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:{' '} - - Learn More - -
- )}
diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 785810ca3..49621e88a 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -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: () => ( + { + 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({ /> -
- 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 ? ( +
+
- 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 ? ( +
+ +
+ +
-
- ) : ( -
- {appConfig.showLoadingIndicator && isLoadingData ? ( - - ) : ( - - )} -
- )} + ) : ( +
+ {appConfig.showLoadingIndicator && isLoadingData ? ( + + ) : ( + + )} +
+ )} +
); diff --git a/platform/app/src/routes/index.tsx b/platform/app/src/routes/index.tsx index d67e8f612..78ad5db47 100644 --- a/platform/app/src/routes/index.tsx +++ b/platform/app/src/routes/index.tsx @@ -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 ( - + { 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); }; diff --git a/platform/app/tailwind.config.js b/platform/app/tailwind.config.js index 8c40c371f..8837fff20 100644 --- a/platform/app/tailwind.config.js +++ b/platform/app/tailwind.config.js @@ -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 }, }, }; diff --git a/platform/app/tailwind.css b/platform/app/tailwind.css index 492349705..510fdf6ec 100644 --- a/platform/app/tailwind.css +++ b/platform/app/tailwind.css @@ -2,3 +2,7 @@ @tailwind base; @tailwind components; @tailwind utilities; + +* { + min-height: 0 !important; +} diff --git a/platform/core/package.json b/platform/core/package.json index 106eeb62e..8f6c6d341 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -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" diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts index 3130a75c4..d9cc2e323 100644 --- a/platform/core/src/classes/MetadataProvider.ts +++ b/platform/core/src/classes/MetadataProvider.ts @@ -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]), diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index d01635538..6c7d7707e 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -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` ); diff --git a/platform/core/src/hooks/useActiveViewportDisplaySets.ts b/platform/core/src/hooks/useActiveViewportDisplaySets.ts new file mode 100644 index 000000000..f5f425249 --- /dev/null +++ b/platform/core/src/hooks/useActiveViewportDisplaySets.ts @@ -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([]); + 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; diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js deleted file mode 100644 index 66d4ac379..000000000 --- a/platform/core/src/index.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts index 219b32098..031d31d3e 100644 --- a/platform/core/src/index.ts +++ b/platform/core/src/index.ts @@ -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; diff --git a/platform/core/src/services/CineService/CineService.ts b/platform/core/src/services/CineService/CineService.ts index 334187d62..e2e652928 100644 --- a/platform/core/src/services/CineService/CineService.ts +++ b/platform/core/src/services/CineService/CineService.ts @@ -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, diff --git a/platform/core/src/services/CustomizationService/CustomizationService.ts b/platform/core/src/services/CustomizationService/CustomizationService.ts index 282011436..e4f8a7766 100644 --- a/platform/core/src/services/CustomizationService/CustomizationService.ts +++ b/platform/core/src/services/CustomizationService/CustomizationService.ts @@ -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, + customComponent?: React.ComponentType + ) { + 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; diff --git a/platform/core/src/services/CustomizationService/types.ts b/platform/core/src/services/CustomizationService/types.ts index 69a2b1f62..da607dec6 100644 --- a/platform/core/src/services/CustomizationService/types.ts +++ b/platform/core/src/services/CustomizationService/types.ts @@ -1,4 +1,4 @@ -import Command from '../../types/Command'; +import { Command } from '../../types/Command'; import { ComponentType } from 'react'; export type Obj = Record; diff --git a/platform/core/src/services/DisplaySetService/DisplaySet.ts b/platform/core/src/services/DisplaySetService/DisplaySet.ts deleted file mode 100644 index 88e5111cd..000000000 --- a/platform/core/src/services/DisplaySetService/DisplaySet.ts +++ /dev/null @@ -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; diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts index 7d72738d4..3bcb79f0a 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.ts +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts @@ -204,7 +204,7 @@ export default class DisplaySetService extends PubSubService { } // If array of instances => One instance. - const displaySetsAdded = []; + const displaySetsAdded = new Array(); if (batch) { for (let i = 0; i < input.length; i++) { diff --git a/platform/core/src/services/HangingProtocolService/HPMatcher.js b/platform/core/src/services/HangingProtocolService/HPMatcher.js index 9d255cc9c..6a874ee47 100644 --- a/platform/core/src/services/HangingProtocolService/HPMatcher.js +++ b/platform/core/src/services/HangingProtocolService/HPMatcher.js @@ -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, }; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 929f31100..10bc77344 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -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; diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 4ebce5d12..4f823c956 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -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; diff --git a/platform/core/src/services/StateSyncService/StateSyncService.test.js b/platform/core/src/services/StateSyncService/StateSyncService.test.js deleted file mode 100644 index 77c23069a..000000000 --- a/platform/core/src/services/StateSyncService/StateSyncService.test.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/platform/core/src/services/StateSyncService/StateSyncService.ts b/platform/core/src/services/StateSyncService/StateSyncService.ts deleted file mode 100644 index ef0d6a362..000000000 --- a/platform/core/src/services/StateSyncService/StateSyncService.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PubSubService } from '../_shared/pubSubServiceInterface'; -import { ExtensionManager } from '../../extensions'; - -const EVENTS = {}; - -type Obj = Record; - -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 { - // 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); - } -} diff --git a/platform/core/src/services/StateSyncService/index.ts b/platform/core/src/services/StateSyncService/index.ts deleted file mode 100644 index 91cc430e5..000000000 --- a/platform/core/src/services/StateSyncService/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StateSyncService from './StateSyncService'; - -export default StateSyncService; diff --git a/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts b/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts index 3edf20525..5b5853d65 100644 --- a/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts +++ b/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts @@ -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, type: string, additionalDetails: Record, @@ -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) { diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts index 287ab1b9e..f8cc6addc 100644 --- a/platform/core/src/services/ToolBarService/ToolbarService.ts +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -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 }); } } diff --git a/platform/core/src/services/ToolBarService/types.ts b/platform/core/src/services/ToolBarService/types.ts index 1318e90f3..3abadb49a 100644 --- a/platform/core/src/services/ToolBarService/types.ts +++ b/platform/core/src/services/ToolBarService/types.ts @@ -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) => { disabled: boolean; className: string; }; +export type EvaluateObject = { + name: string; + // Allow any additional properties + [key: string]: unknown; +}; + export type ButtonProps = { id: string; icon: string; diff --git a/platform/core/src/services/UINotificationService/index.ts b/platform/core/src/services/UINotificationService/index.ts index b49f46fa7..f1f565d81 100644 --- a/platform/core/src/services/UINotificationService/index.ts +++ b/platform/core/src/services/UINotificationService/index.ts @@ -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; + 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, diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index 4f31ba051..856bb3223 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -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 }; diff --git a/platform/core/src/services/ViewportGridService/getPresentationIds.ts b/platform/core/src/services/ViewportGridService/getPresentationIds.ts deleted file mode 100644 index 835442879..000000000 --- a/platform/core/src/services/ViewportGridService/getPresentationIds.ts +++ /dev/null @@ -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 }; diff --git a/platform/core/src/services/ViewportGridService/index.ts b/platform/core/src/services/ViewportGridService/index.ts index 38f365814..47d083531 100644 --- a/platform/core/src/services/ViewportGridService/index.ts +++ b/platform/core/src/services/ViewportGridService/index.ts @@ -1,5 +1,3 @@ import ViewportGridService from './ViewportGridService'; -import type { PresentationIds } from './ViewportGridService'; export default ViewportGridService; -export type { PresentationIds }; diff --git a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts index 5165e55a1..bd95b5326 100644 --- a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts +++ b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts @@ -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); }); } diff --git a/platform/core/src/services/index.ts b/platform/core/src/services/index.ts index 68666a18a..c2df4190e 100644 --- a/platform/core/src/services/index.ts +++ b/platform/core/src/services/index.ts @@ -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, diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts index 579dfa313..07db43b48 100644 --- a/platform/core/src/types/AppTypes.ts +++ b/platform/core/src/types/AppTypes.ts @@ -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 | 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; + 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; - studyPrefetcher: { + onConfiguration?: ( + dicomWebConfig: Record, + options: Record + ) => Record; + dataSources?: Record; + oidc?: Record; + peerImport?: (moduleId: string) => Promise>; + 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 = HangingProtocolTypes.CustomOptionAttribute; + export type CustomOption = HangingProtocolTypes.CustomOption; + 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 & AppTypes.Services & AppTypes.Managers & { - [key: string]: any; - }; + [key: string]: unknown; + } & AppTypes.Config; export type withTestTypes = T & AppTypes.Test; } diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts index 3bc1cb516..e11cc0bef 100644 --- a/platform/core/src/types/DisplaySet.ts +++ b/platform/core/src/types/DisplaySet.ts @@ -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 = { diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index 16b846fb2..1e6e2e4ba 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -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; id?: string; orientation?: CustomOption; + background?: CustomOption<[number, number, number]>; viewportId?: string; displayArea?: DisplayArea; initialImageOptions?: CustomOption; @@ -189,9 +197,17 @@ export type DisplaySetOptions = { options?: Record; }; +// some options for overlays +// such as segmentation options +export type OverlayOptions = { + id?: string; + options?: Record; +}; + 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; + /** overlay selectors that decide whether an overlay such as segmentation should be shown or not */ + overlaySelectors?: Record; /** A default viewport to use for any stage to select new viewport layouts. */ defaultViewport?: Viewport; stages: ProtocolStage[]; diff --git a/platform/core/src/types/Services.ts b/platform/core/src/types/Services.ts index 119a2da87..b8a514961 100644 --- a/platform/core/src/types/Services.ts +++ b/platform/core/src/types/Services.ts @@ -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; diff --git a/platform/core/src/types/ViewportGridType.ts b/platform/core/src/types/ViewportGridType.ts new file mode 100644 index 000000000..1079a8bcd --- /dev/null +++ b/platform/core/src/types/ViewportGridType.ts @@ -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; + // + 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; + +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; + }>; +}>; diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts index 3e1608790..e01e64f71 100644 --- a/platform/core/src/types/index.ts +++ b/platform/core/src/types/index.ts @@ -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, }; diff --git a/platform/core/src/utils/combineFrameInstance.ts b/platform/core/src/utils/combineFrameInstance.ts index 087f97b42..6385bfbd0 100644 --- a/platform/core/src/utils/combineFrameInstance.ts +++ b/platform/core/src/utils/combineFrameInstance.ts @@ -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; } diff --git a/platform/core/src/utils/createStudyBrowserTabs.ts b/platform/core/src/utils/createStudyBrowserTabs.ts index 0d55149e3..259558ac0 100644 --- a/platform/core/src/utils/createStudyBrowserTabs.ts +++ b/platform/core/src/utils/createStudyBrowserTabs.ts @@ -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 = []; diff --git a/platform/core/src/utils/getImageId.js b/platform/core/src/utils/getImageId.js index 51e057b91..d40f127d7 100644 --- a/platform/core/src/utils/getImageId.js +++ b/platform/core/src/utils/getImageId.js @@ -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(); } diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 22ae507ae..85d18f71d 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -41,7 +41,6 @@ describe('Top level exports', () => { 'resolveObjectPath', 'hierarchicalListUtils', 'progressTrackingUtils', - 'subscribeToNextViewportGridChange', 'uuidv4', 'addAccessors', ].sort(); diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts index e5725f748..74c9cdd38 100644 --- a/platform/core/src/utils/index.ts +++ b/platform/core/src/utils/index.ts @@ -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, diff --git a/platform/core/src/utils/roundNumber.js b/platform/core/src/utils/roundNumber.js index 64f614a86..3747e5159 100644 --- a/platform/core/src/utils/roundNumber.js +++ b/platform/core/src/utils/roundNumber.js @@ -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); } diff --git a/platform/core/src/utils/subscribeToNextViewportGridChange.ts b/platform/core/src/utils/subscribeToNextViewportGridChange.ts deleted file mode 100644 index 6ffbc4a1c..000000000 --- a/platform/core/src/utils/subscribeToNextViewportGridChange.ts +++ /dev/null @@ -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 }; diff --git a/platform/core/src/utils/toNumber.js b/platform/core/src/utils/toNumber.js index 986a8d518..b0d3bd0dd 100644 --- a/platform/core/src/utils/toNumber.js +++ b/platform/core/src/utils/toNumber.js @@ -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; } diff --git a/platform/docs/docs/configuration/dataSources/dicom-web.md b/platform/docs/docs/configuration/dataSources/dicom-web.md index 8e2d49250..d6fd1ec1c 100644 --- a/platform/docs/docs/configuration/dataSources/dicom-web.md +++ b/platform/docs/docs/configuration/dataSources/dicom-web.md @@ -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. diff --git a/platform/docs/docs/development/webWorkers.md b/platform/docs/docs/development/webWorkers.md new file mode 100644 index 000000000..7fa3bed1a --- /dev/null +++ b/platform/docs/docs/development/webWorkers.md @@ -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); +``` diff --git a/platform/docs/docs/faq/technical.md b/platform/docs/docs/faq/technical.md index 33a50df51..4b446e5ab 100644 --- a/platform/docs/docs/faq/technical.md +++ b/platform/docs/docs/faq/technical.md @@ -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'], + }, +]); +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md b/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md new file mode 100644 index 000000000..117e96ce4 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md @@ -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? + + + + +```md +- React 17 +- Using `defaultProps` +- `babel-inline-svg` for SVG imports +``` + + + + +```md +- React 18 +- Default parameters for props +- `svgr` for SVG imports +``` + + + + + +## 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: + + + + +```jsx +const MyComponent = ({ prop1, prop2 }) => { + return
{prop1} {prop2}
+} + +MyComponent.defaultProps = { + prop1: 'default value', + prop2: 'default value' +} +``` + +
+ + +```jsx +const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => { + return
{prop1} {prop2}
+} +``` +
+
+ +## 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. + + + + +```javascript +import arrowDown from './../../assets/icons/arrow-down.svg'; +``` + + + + +```javascript +import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg'; +``` + + + + +--- + +## 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'], + } + ``` + +
+Example Migration + +Before: + +```json +evaluate: ['evaluate.cine', 'evaluate.not3D'], +``` + +After + +```json +evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, +], +``` +
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md new file mode 100644 index 000000000..c8a92efa0 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md @@ -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 + +
+Read More + +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/ + + + +
+ + + + +### 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 diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md new file mode 100644 index 000000000..06601b7cd --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md @@ -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; +}; +``` + + +
+Compared to Cornerstone3D 1.x + +Previously this function was returning this + +```js +export type Segmentation = { + segmentationId: string; + type: Enums.SegmentationRepresentations; + label: string; + activeSegmentIndex: number; + segmentsLocked: Set; + 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. + +
+ +--- + +## 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); +} +``` + +
+Key Changes + +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 +
+ + +
+Migration Examples + +**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'); + +``` + +
+ +--- + +## 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 +``` + +
+Migration Examples + +1. **Basic Usage Update** + + ```js + // Before - OHIF 3.8 + segmentationService.setActiveSegmentationForToolGroup( + segmentationId, + toolGroupId + ); + // After - OHIF 3.9 + segmentationService.setActiveSegmentation( + viewportId, + segmentationId + ); + ``` + +
+ + + +--- + + +## 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 +``` + +
+Key Changes + +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 +
+ + + + +
+Migration Examples + +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 + }); + ``` + +
+ + + + +
+Important Changes + +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 + }); + ``` + +
+ + +--- + +--- + +## 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 +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md new file mode 100644 index 000000000..522259276 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md @@ -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 +}); +``` + +
+Benefits of the New Approach + +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 +
+ + +
+Migration Tips + +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 + +
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md new file mode 100644 index 000000000..d96f9a014 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md @@ -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 +``` + +**After (OHIF 3.9)** + +```js +// Method 1: Display Set Based +async createLabelmapForDisplaySet( + displaySet: DisplaySet, + options?: { + segmentationId?: string; + label: string; + segments?: { + [segmentIndex: number]: Partial + }; + } +): Promise +``` + + +
+Migration Examples + + +```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' + } +); +``` + +
+ +--- + +## createSegmentationForRTDisplaySet + + +**Before (OHIF 3.8)** + +```js +async createSegmentationForRTDisplaySet( + rtDisplaySet, + segmentationId?: string, + suppressEvents = false +): Promise +``` + +**After (OHIF 3.9)** + +```js +async createSegmentationForRTDisplaySet( + rtDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; // not required, defaults to Contour + } +): Promise +``` + + +
+Migration Examples + +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 + } +); +``` + +
+ +--- + + +## createSegmentationForSEGDisplaySet Changes + +**Before (OHIF 3.8)** + +```js +async createSegmentationForSEGDisplaySet( + segDisplaySet, + segmentationId?: string, + suppressEvents = false +): Promise +``` + +**After (OHIF 3.9)** + +```js +async createSegmentationForSEGDisplaySet( + segDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; // not required, defaults to Labelmap + } +): Promise +``` + +
+Migration Examples + +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 + } + ); + ``` +
+ + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md new file mode 100644 index 000000000..ebe16f7c1 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md @@ -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 +}); +``` + +
+Benefits of the New Approach + +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 +
+ + +
+Migration Tips + +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 + +
+ + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md new file mode 100644 index 000000000..266206bf1 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md @@ -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 +``` + +
+Migration Example + +```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 + ); +}); +``` + + +
+ + +### 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); +``` + + +
+Migration Examples + +**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 + } +); +``` + +
+ + + +**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: + + +
+Additional Notes + + +- 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 })` +
+ +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md new file mode 100644 index 000000000..1236787e1 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md @@ -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 +) +``` + +### 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 + } + } + } +}; +``` + +
+Migration Examples + + +```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 }, + } + } +}); +``` + +
+ + +## 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' +) +``` + +
+Key Changes + +1. Removed `toolGroupId` in favor of `viewportId` +2. If no viewportId is provided, highlights in all relevant viewports + +
+ +
+Migration Examples + +**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 + ); +}); +``` +
+ +--- + +## 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' +) +``` + +
+Key Changes + +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' +); +``` + +
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md new file mode 100644 index 000000000..0eea04b41 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md @@ -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! +::: diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md b/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md new file mode 100644 index 000000000..6d8b1ce0f --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md @@ -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 + + + + @ohif/extension-default.panelModule.measure + + + @ohif/extension-default.panelModule.measurements + + diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md b/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md new file mode 100644 index 000000000..2d14adf53 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md @@ -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? + + + + +```javascript +useBulkDataURI: false, +``` + + + + +```javascript +bulkDataURI: { + enabled: true, + // Additional configuration **options** +}, +``` + + + + + +**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. diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md b/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md new file mode 100644 index 000000000..d4b89faf3 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md @@ -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 + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md b/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md new file mode 100644 index 000000000..7b85dafc0 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md @@ -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: , + location: viewportActionCornersService.LOCATIONS.topRight +}); +``` + +**New API** + +```js +viewportActionCornersService.addComponent({ + viewportId, + id: 'myComponent', + component: , + location: viewportActionCornersService.LOCATIONS.topRight, + indexPriority: 1, // indexPriority is now optional and determines placement order within the corner +}); + +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md b/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md new file mode 100644 index 000000000..8505a342e --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md @@ -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. +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md b/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md new file mode 100644 index 000000000..f13bf9130 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md @@ -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 diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md b/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md new file mode 100644 index 000000000..2dbb01c42 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md @@ -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. + + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md b/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md new file mode 100644 index 000000000..795b3e3c6 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md @@ -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'); + }, + }, +]); + +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md b/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md new file mode 100644 index 000000000..5bd8b735d --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md @@ -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 + +``` + +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. diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/index.md b/platform/docs/docs/migration-guide/3p8-to-3p9/index.md new file mode 100644 index 000000000..56e414e87 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/index.md @@ -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. diff --git a/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md b/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md deleted file mode 100644 index d430f06a1..000000000 --- a/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md +++ /dev/null @@ -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? - - - -```md -- React 17 -- Using `defaultProps` -- `babel-inline-svg` for SVG imports -``` - - - - -```md -- React 18 -- Default parameters for props -- `svgr` for SVG imports -``` - - - - - -### 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: - - - - -```jsx -const MyComponent = ({ prop1, prop2 }) => { - return
{prop1} {prop2}
-} - -MyComponent.defaultProps = { - prop1: 'default value', - prop2: 'default value' -} -``` - -
- - -```jsx -const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => { - return
{prop1} {prop2}
-} -``` -
-
- -### Update SVG imports: - - - - -```javascript -import arrowDown from './../../assets/icons/arrow-down.svg'; -``` - - - - -```javascript -import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg'; -``` - - - - - -
- ---- - -
- -## 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 - - - - @ohif/extension-default.panelModule.measure - - - @ohif/extension-default.panelModule.measurements - - - - -
- ---- - -
- -## 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 - -
- ---- - -
- -## 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, - }, - } -} -``` - -
- ---- - -
- -## 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: - -``` - -``` - -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? - - - - -```javascript -useBulkDataURI: false, -``` - - - - -```javascript -bulkDataURI: { - enabled: true, - // Additional configuration options -}, -``` - - - - - -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. - - -
- ---- - -
- -## 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-). - - -
- ---- - -
- -## 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. diff --git a/platform/docs/docs/platform/extensions/modules/toolbar.md b/platform/docs/docs/platform/extensions/modules/toolbar.md index 94fdb22fc..b353a0304 100644 --- a/platform/docs/docs/platform/extensions/modules/toolbar.md +++ b/platform/docs/docs/platform/extensions/modules/toolbar.md @@ -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: diff --git a/platform/docs/docs/platform/services/data/SegmentationService.md b/platform/docs/docs/platform/services/data/SegmentationService.md index bd99e12de..31e4b6726 100644 --- a/platform/docs/docs/platform/services/data/SegmentationService.md +++ b/platform/docs/docs/platform/services/data/SegmentationService.md @@ -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. diff --git a/platform/docs/docs/platform/services/data/StateSyncService.md b/platform/docs/docs/platform/services/data/StateSyncService.md deleted file mode 100644 index 49fdbf0c3..000000000 --- a/platform/docs/docs/platform/services/data/StateSyncService.md +++ /dev/null @@ -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 diff --git a/platform/docs/docs/platform/services/ui/viewport-action-menu.md b/platform/docs/docs/platform/services/ui/viewport-action-menu.md new file mode 100644 index 000000000..5a0592535 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/viewport-action-menu.md @@ -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. diff --git a/platform/docs/docusaurus.config.js b/platform/docs/docusaurus.config.js index 4f295febd..092b18377 100644 --- a/platform/docs/docusaurus.config.js +++ b/platform/docs/docusaurus.config.js @@ -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: '', }, { - to: '/versions', - label: 'All versions', + type: 'html', + className: 'dropdown-archived-versions', + value: 'Archived versions', }, + ...ArchivedVersionsDropdownItems.map(item => ({ + label: `${item.version} `, + href: item.href, + target: item.isExternal ? '_blank' : undefined, + rel: item.isExternal ? 'noopener noreferrer' : undefined, + })), ], }, { diff --git a/platform/docs/package.json b/platform/docs/package.json index aee01682b..51f06a0fb 100644 --- a/platform/docs/package.json +++ b/platform/docs/package.json @@ -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", diff --git a/platform/docs/src/css/custom.css b/platform/docs/src/css/custom.css index fa0b0ad77..5d7b1b8eb 100644 --- a/platform/docs/src/css/custom.css +++ b/platform/docs/src/css/custom.css @@ -537,3 +537,130 @@ div[class^='announcementBar_'] { ); font-weight: 700; } + +/* #__docusaurus { + height: 100%; +} */ + +.dropdown-separator { + border-top: 1px solid #808080; +} + +/* flex items , center */ +.dropdown__link { + display: flex; + align-items: center; +} + +.footer__link-item { + display: flex; + align-items: center; +} + +/* add proper ui link styling */ + +/* Bullet point styling */ +ul { + list-style-type: disc; + padding-left: 1.5rem; + margin: 1rem 0; +} + +ul li { + margin-bottom: 0.5rem; +} + +/* Nested bullet points */ +ul ul { + list-style-type: circle; + margin: 0.5rem 0; +} + +/* For documentation bullet points specifically */ +.markdown ul { + list-style-type: disc; + padding-left: 1.5rem; +} + +.markdown ul li { + margin-bottom: 0.5rem; +} + +/* Markdown link styling */ +.markdown a { + color: #0066cc; + text-decoration: none; + transition: color 0.2s ease; +} + +.markdown a:hover { + color: #0051a3; + text-decoration: underline; +} + +/* Dark mode link styling */ +html[data-theme='dark'] .markdown a { + color: #66b3ff; +} + +html[data-theme='dark'] .markdown a:hover { + color: #99ccff; +} + +/* Horizontal rule styling */ +.markdown hr { + height: 1px; + border: none; + background: linear-gradient(to right, #0066cc, #66b3ff); + margin: 2rem 0; + opacity: 0.6; +} + +/* Dark mode horizontal rule */ +html[data-theme='dark'] .markdown hr { + background: linear-gradient(to right, #66b3ff, #99ccff); +} + +/* Markdown code block styling */ +.markdown pre { + font-size: 0.9rem; +} + +.theme-code-block { + font-size: 0.9rem; +} + +/* Target both light and dark themes */ +[data-theme='light'] .theme-code-block, +[data-theme='dark'] .theme-code-block { + font-size: 0.9rem; +} + +/* Dropdown menu positioning and interaction fixes */ +.dropdown { + position: relative; +} + +.dropdown__menu { + top: 100%; + margin-top: 0; + padding-top: 0.5rem; +} + +/* Add a hover area to prevent menu from disappearing */ +.dropdown__menu::before { + content: ''; + position: absolute; + top: -10px; + left: 0; + right: 0; + height: 10px; +} + +/* Ensure menu stays visible while hovering */ +.dropdown:hover .dropdown__menu, +.dropdown__menu:hover { + opacity: 1; + visibility: visible; + transform: translateY(0); +} diff --git a/platform/docs/src/pages/patterns/patterns-measurements.tsx b/platform/docs/src/pages/patterns/patterns-measurements.tsx index a0ea5cc8d..343bd908d 100644 --- a/platform/docs/src/pages/patterns/patterns-measurements.tsx +++ b/platform/docs/src/pages/patterns/patterns-measurements.tsx @@ -9,6 +9,7 @@ import { } from '../../../../ui-next/src/components/Accordion'; import { DataRow } from '../../../../ui-next/src/components/DataRow'; import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; +import BrowserOnly from '@docusaurus/BrowserOnly'; interface DataItem { id: number; @@ -36,95 +37,95 @@ export default function Measurements() { }; const organSegmentationGroup = dataList.find( - (listGroup: ListGroup) => listGroup.type === 'Organ Segmentation' + listGroup => listGroup.type === 'Organ Segmentation' ); - const roiToolsGroup = dataList.find((listGroup: ListGroup) => listGroup.type === 'ROI Tools'); + const roiToolsGroup = dataList.find(listGroup => listGroup.type === 'ROI Tools'); - if (!organSegmentationGroup) { - return
Organ Segmentation data not found.
; - } - - if (!roiToolsGroup) { - return
ROI Tools data not found.
; + if (!organSegmentationGroup || !roiToolsGroup) { + return null; // Avoid rendering until these groups are ready. } return ( -
- {/* Simulated Panel List for "Segmentation" */} -
- - {/* Segmentation Tools */} - - - Measurements - - -
-
2024-Jan-01
-
- Study title lorem ipsum -
-
+ + {() => ( +
+ {/* Simulated Panel List for "Segmentation" */} +
+ + {/* Segmentation Tools */} + + + Measurements + + +
+
2024-Jan-01
+
+ Study title lorem ipsum +
+
-
-
- - -
-
-
- {roiToolsGroup.items.map((item, index) => { - const compositeId = `${roiToolsGroup.type}-${item.id}-panel`; // Ensure unique composite ID - return ( - handleAction(compositeId, action)} - isSelected={selectedRowId === compositeId} - onSelect={() => handleRowSelect(compositeId)} - /> - ); - })} -
-
-
+
+
+ + +
+
+
+ {roiToolsGroup.items.map((item, index) => { + const compositeId = `${roiToolsGroup.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+ + - {/* Additional Findings */} - - - Additional Findings - - -
-
-
-
-
-
+ {/* Additional Findings */} + + + Additional Findings + + +
+
+
+
+
+
+ )} + ); } diff --git a/platform/docs/src/pages/patterns/patterns-segmentation.tsx b/platform/docs/src/pages/patterns/patterns-segmentation.tsx index 49f6c07e2..04d0088f7 100644 --- a/platform/docs/src/pages/patterns/patterns-segmentation.tsx +++ b/platform/docs/src/pages/patterns/patterns-segmentation.tsx @@ -1,3 +1,5 @@ +'use client'; + import React, { useState } from 'react'; import { DataRow } from '../../../../ui-next/src/components/DataRow'; @@ -34,6 +36,7 @@ import { Label } from '../../../../ui-next/src/components/Label'; import { Input } from '../../../../ui-next/src/components/Input'; import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; interface DataItem { id: number; @@ -74,232 +77,234 @@ export default function SegmentationPanel() { return (
- - {/* Segmentation Tools */} - - - Segmentation Tools - - -
-
-
+ + + {/* Segmentation Tools */} + + + Segmentation Tools + + +
+
+
- {/* Segmentation List */} - - - Segmentation List - - -
- {/* Header Controls */} -
- - - - - - - - Create New Segmentation - - - Manage Current Segmentation - - - Remove from Viewport - - - - Rename - - - - - Export & Download - - - - Export DICOM SEG - Download DICOM SEG - Download DICOM RTSTRUCT - - - - - - - Delete - - - - - -
- - {/* Appearance Settings */} - - -
- - Appearance Settings -
-
- -
-
- {/* Display Label with Selected Tab */} -
Show: {selectedTab}
- {/* Tabs Controls */} - + + Segmentation List + + +
+ {/* Header Controls */} +
+ + +
- {/* Opacity Slider */} -
- - - -
- {/* Border Slider */} -
- - - -
- {/* Sync Changes Switch */} -
- - -
-
- {/* Display Inactive Segmentations Switch */} -
- - -
- {/* Additional Opacity Slider */} -
- - - -
-
-
- - {/* Action Buttons */} -
- - -
-
+ + + + + + + Create New Segmentation + + + Manage Current Segmentation + + + Remove from Viewport + + + + Rename + + + + + Export & Download + + + + Export DICOM SEG + Download DICOM SEG + Download DICOM RTSTRUCT + + + + + + + Delete + + + + + +
- {/* Data Rows */} -
- {organSegmentationGroup.items.map((item, index) => { - const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID - return ( - handleAction(compositeId, action)} - isSelected={selectedRowId === compositeId} - onSelect={() => handleRowSelect(compositeId)} - /> - ); - })} -
-
-
- + {/* Appearance Settings */} + + +
+ + Appearance Settings +
+
+ +
+
+ {/* Display Label with Selected Tab */} +
Show: {selectedTab}
+ {/* Tabs Controls */} + + + + + + + + + + + + + +
+ {/* Opacity Slider */} +
+ + + +
+ {/* Border Slider */} +
+ + + +
+ {/* Sync Changes Switch */} +
+ + +
+
+ {/* Display Inactive Segmentations Switch */} +
+ + +
+ {/* Additional Opacity Slider */} +
+ + + +
+
+
+
+ {/* Action Buttons */} +
+ + +
+
+ + {/* Data Rows */} +
+ {organSegmentationGroup.items.map((item, index) => { + const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+
+
+
); diff --git a/platform/docs/src/pages/patterns/patterns-tmtv.tsx b/platform/docs/src/pages/patterns/patterns-tmtv.tsx index 0040971b5..b792a8fe4 100644 --- a/platform/docs/src/pages/patterns/patterns-tmtv.tsx +++ b/platform/docs/src/pages/patterns/patterns-tmtv.tsx @@ -1,25 +1,12 @@ import React, { useState } from 'react'; import { Button } from '../../../../ui-next/src/components/Button'; -import { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -} from '../../../../ui-next/src/components/Select'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, - DropdownMenuLabel, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, @@ -39,9 +26,9 @@ import { Slider } from '../../../../ui-next/src/components/Slider'; import { Switch } from '../../../../ui-next/src/components/Switch'; import { Label } from '../../../../ui-next/src/components/Label'; import { Input } from '../../../../ui-next/src/components/Input'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../ui-next/src/components/Tabs'; - -import { ChevronDownIcon } from '@radix-ui/react-icons'; +import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; +import { TooltipProvider } from '../../../../ui-next/src'; +import BrowserOnly from '@docusaurus/BrowserOnly'; interface DataItem { id: number; @@ -88,316 +75,320 @@ export default function TMTVPatterns() { } return ( -
-
- - {/* Segmentation Tools */} - - - Segmentation Tools - - -
-
-
- {/* Segmentation List */} - - - Segmentation List - - - {/* Appearance Settings */} - - -
- - Appearance Settings -
+ + {() => ( +
+
+ + {/* Segmentation Tools */} + + + Segmentation Tools -
-
- {/* Display Label with Selected Tab */} -
Show: {selectedTab}
- {/* Tabs Controls */} - - - - - - - - - - - - - -
- {/* Opacity Slider */} -
- - - -
- {/* Border Slider */} -
- - - -
- {/* Sync Changes Switch */} -
- - -
-
- {/* Display Inactive Segmentations Switch */} -
- - -
- {/* Additional Opacity Slider */} -
- - - -
-
+
- {/* TMTV1 Group */} - - -
- {/* Left Group: DropdownMenu and TMTV1 Label */} -
- - + {/* Segmentation List */} + + + Segmentation List + + + {/* Appearance Settings */} + + +
+ + Appearance Settings +
+
+ +
+
+ {/* Display Label with Selected Tab */} +
Show: {selectedTab}
+ {/* Tabs Controls */} + + + + + + + + + + + + + +
+ {/* Opacity Slider */} +
+ + + +
+ {/* Border Slider */} +
+ + + +
+ {/* Sync Changes Switch */} +
+ + +
+
+ {/* Display Inactive Segmentations Switch */} +
+ + +
+ {/* Additional Opacity Slider */} +
+ + + +
+
+
+
+ {/* TMTV1 Group */} + + +
+ {/* Left Group: DropdownMenu and TMTV1 Label */} +
+ + + + + + + + Add Segment + + + + + Remove from Viewport + + + + Rename + + + + Hide or Show all Segments + + + + + Export & Download + + + + Export DICOM SEG + Download DICOM SEG + Download DICOM RTSTRUCT + + + + + + + Delete + + + +
TMTV1 Segmentation
+
+
- - - - - Add Segment - - - - - Remove from Viewport - - - - Rename - - - - Hide or Show all Segments - - - - - Export & Download - - - - Export DICOM SEG - Download DICOM SEG - Download DICOM RTSTRUCT - - - - - - - Delete - - - -
TMTV1 Segmentation
-
-
- -
-
-
- - {/* Data Rows for TMTV1 */} -
- {tmvGroup.items.map((item, index) => { - const compositeId = `${tmvGroup.type}-${item.id}-panel`; // Ensure unique composite ID - return ( - handleAction(compositeId, action)} - isSelected={selectedRowId === compositeId} - onSelect={() => handleRowSelect(compositeId)} - /> - ); - })} -
-
-
- {/* TMTV2 Group */} - - -
-
- - - - - - - - Add Segment - - - - - Remove from Viewport - - - - Rename - - - - Hide or Show all Segments - - - - - Export & Download - - - - Export DICOM SEG - Download DICOM SEG - Download DICOM RTSTRUCT - - - - - - - Delete - - - +
+
+
+ + {/* Data Rows for TMTV1 */} +
+ {tmvGroup.items.map((item, index) => { + const compositeId = `${tmvGroup.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+
+ {/* TMTV2 Group */} + + +
+
+ + + + + + + + Add Segment + + + + + Remove from Viewport + + + + Rename + + + + Hide or Show all Segments + + + + + Export & Download + + + + Export DICOM SEG + Download DICOM SEG + Download DICOM RTSTRUCT + + + + + + + Delete + + + -
TMTV2 Segmentation
-
-
- -
-
-
- - {/* Data Rows for TMTV2 */} -
- {tmv2Group.items.map((item, index) => { - const compositeId = `${tmv2Group.type}-${item.id}-panel`; // Ensure unique composite ID - return ( - handleAction(compositeId, action)} - isSelected={selectedRowId === compositeId} - onSelect={() => handleRowSelect(compositeId)} - /> - ); - })} +
TMTV2 Segmentation
+
+
+ +
+
+ + + {/* Data Rows for TMTV2 */} +
+ {tmv2Group.items.map((item, index) => { + const compositeId = `${tmv2Group.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+ + {/* Footer or Additional Information */} +
+ TMTV + 21.555 mL
- {/* Footer or Additional Information */} -
- TMTV - 21.555 mL -
- - - -
-
+ +
+
+ )} + ); } diff --git a/platform/docs/src/pages/versions.js b/platform/docs/src/pages/versions.js new file mode 100644 index 000000000..54e777994 --- /dev/null +++ b/platform/docs/src/pages/versions.js @@ -0,0 +1,63 @@ +import React from 'react'; +import Layout from '@theme/Layout'; +import Link from '@docusaurus/Link'; + +export default function Versions() { + const versions = [ + { + version: 'Version 1', + status: 'deprecated', + description: 'Built with Meteor as a full stack application.', + }, + { + version: 'Version 2', + status: 'deprecated', + description: 'Front end image viewer built with React', + }, + { + version: 'Version 3.x-beta', + status: 'master branch', + description: 'With latest bug fixes and features but not yet released (released under beta)', + }, + { + version: 'Version 3.x', + status: 'release branch', + description: 'Released version of the OHIF platform which is more stable and tested', + }, + ]; + + return ( + +
+

Versions

+ +

+ As we are increasing the efforts to make the OHIF platform more robust and up-to-date with + the latest software engineering practices, here we are listing the versions of the OHIF + platform that we are currently supporting, and the versions that have been deprecated. +

+ +

Product Version

+ +

Currently we have four product versions:

+ +
    + {versions.map((item, index) => ( +
  • + {item.version} ({item.status}): {item.description} +
  • + ))} +
+ +

+ You can read more about the differences between the versions in the{' '} + development section of the + documentation to understand which version is more suitable for your use case. +

+
+
+ ); +} diff --git a/platform/docs/src/pages/versions.md b/platform/docs/src/pages/versions.md deleted file mode 100644 index 7f985e56f..000000000 --- a/platform/docs/src/pages/versions.md +++ /dev/null @@ -1,20 +0,0 @@ -# Versions - -As we are increasing the efforts to make the OHIF platform more robust and up-to-date -with the latest software engineering practices, here we are listing the versions of -the OHIF platform that we are currently supporting, and the versions that have been -deprecated. - -## Product Version - - -Currently we have three product versions: - -- Version 1 (deprecated): Built with Meteor as a full stack application. -- Version 2 (deprecated): Front end image viewer built with React -- Version 3.x-beta (master branch): With latest bug fixes and features but not yet released (released under beta) -- Version 3.x (release branch): Released version of the OHIF platform which is more stable and tested - - -You can read more about the differences between the versions in the [development section](../../docs/development/getting-started#branches) of the documentation -to understand which version is more suitable for your use case. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/_category_.json deleted file mode 100644 index c1a514219..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "I Want to ...", - "position": 8 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-logo-to-the-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-logo-to-the-viewer.md deleted file mode 100644 index 53942222e..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-logo-to-the-viewer.md +++ /dev/null @@ -1,40 +0,0 @@ -# Add a Logo to the Viewer - -The OHIF Framework provides [**ohif-header**](https://github.com/OHIF/Viewers/tree/master/Packages/ohif-header) package to add a header into application layout. **ohif-header** package is designed as a [custom block helper](http://blazejs.org/api/spacebars.html#Custom-Block-Helpers) named **header** to define your own header context. - -If you would like to add SVG logo to header please follow these steps. - -1. Add your SVG logo into public folder. - -2. Add **header** content block which will be located on the top of the application layout into your application's main template. You can also pass some parameters to **header** content block to customize the header. - - * headerClasses: the list of classes which will be applied to header element - * brandHref: the url of the logo to link - - -``` html -{{#header headerClasses="header-big bg-blue" brandHref="your logo link"}} -... -{{/header}} -``` - - -1. Create a section called as **brand** in **header** content block and add your logo content which is displayed on the left side of the header as default into section **brand**. - - - ``` html - {{#header}} - {{#section "brand"}} - - - - - - -
Logo Text
- {{/section}} - {{/header}} - ``` - - - For example, see how it works in [OHIF Viewer](https://github.com/OHIF/Viewers/blob/master/OHIFViewer/client/components/ohifViewer/ohifViewer.html#L2) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-tool-to-the-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-tool-to-the-viewer.md deleted file mode 100644 index a55a32628..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-tool-to-the-viewer.md +++ /dev/null @@ -1,22 +0,0 @@ -# Add a Tool to the Viewer - -To add a tool to the Viewer there are a few steps: - -1. Add the tool itself to the repository. - - If you're using something from Cornerstone Tools you can skip this step. - - Some examples of custom tools can be found in the lesion tracker: https://github.com/OHIF/Viewers/tree/master/Packages/ohif-lesiontracker/client/compatibility - -2. Add the toolbar button itself to the array of tools in the Toolbar: - https://github.com/OHIF/Viewers/blob/574a6d02b090b8b2f020430c5919f8377b8316c6/OHIFViewer/client/components/toolbarSection/toolbarSection.js - -3. **A:** Add it to the toolManager (if it's a tool, such as length / angle): - - https://github.com/OHIF/Viewers/blob/574a6d02b090b8b2f020430c5919f8377b8316c6/Packages/lesiontracker/client/tools.js#L2 - - ** --- OR --- ** - - **B:** Add it to the functionList if it's a command (e.g. toggle CINE play, or Invert the current viewport): - - https://github.com/OHIF/Viewers/blob/574a6d02b090b8b2f020430c5919f8377b8316c6/OHIFViewer/client/components/viewer/viewer.js#L12 diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/Standalone-Installation-Instructions.md b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/Standalone-Installation-Instructions.md deleted file mode 100644 index fb6213184..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/Standalone-Installation-Instructions.md +++ /dev/null @@ -1,61 +0,0 @@ -# OHIF Standalone Installation Instructions For Windows Server 2016: - -**Note: Turn-Off Windows Defender Realtime protection during this process** - -1. Install Chocolatey - - a. Go to this URL for instructions: - - https://chocolatey.org/install#install-with-cmdexe - - b. Execute this command in the cmd line as Admin: - ``` - @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" - ``` - - c. Type `choco -?` to ensure the installation is OK. -2. Install Meteor - - a. Type the command: `choco install meteor` -3. Install node.js - - a. Download MSI from: https://nodejs.org/en/download/ -4. Install MongoDB - - a. Download MSI from: -https://www.mongodb.com/download-center/community -5. Install Git - - a. https://git-scm.com/download/win -6. Download the OHIF Viewer repository from GitHub, or use Git to clone it (recommended) - - a. GitHub Repo: https://github.com/OHIF/Viewers - - b. GIT Clone command: `git clone https://github.com/OHIF/Viewers` -7. Set the Meteor Packages folder environment variable - - a. Go to Advanced System Settings - - b. Under Advanced, click on the "Environment Variables..." button - - c. Under System Variables, click "New..." - - d. Set the following: - - i. Variable name: METEOR_PACKAGE_DIRS - - ii. Variable Value: - example: C:\OHIF\Viewers\Packages -8. Using the command line, navigate/cs to the standalone viewer folder, example: `cd C:\OHIF\Viewers\StandaloneViewer\StandaloneViewer` -9. run the command: `meteor npm install` -10. Run the command: `meteor` - - a. if you get this error "Error: EPERM: operation not permitted, unlink" Or, - - b. You feel the build is stale for a very long time > 10min with no visual indication, restart the cmd prompt and repeat this step (`meteor`) - -# Troubleshooting: - -1. If you get an error: Error: EPERM: operation not permitted, unlink `c:\xxxxx` - - a. https://github.com/phoenixframework/phoenix/issues/2464 diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/_category_.json deleted file mode 100644 index 787010486..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Extras", - "position": 13 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/environment-installations.md b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/environment-installations.md deleted file mode 100644 index 09dd39744..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/environment-installations.md +++ /dev/null @@ -1,345 +0,0 @@ ---- -title: Development Environment Installation ---- - -**Development Environment Installation (Ubuntu)** - -**CONFIDENTIAL DOCUMENT** - -This is a confidential document and property of Radical Imaging LLC. It shall not be transmitted, copied or sent to anyone without prior authorization. - -**Index** - -[[TOC]] - - - - - - - - - - - - - - -
Acronyms and definitions
Acronym/DefinitionMeaning
N/AN/A
- - - - - - - - - - - - - - - -
Referenced Documents
Doc NoDoc Title
DOC00001Radical Imaging Official Documents Template
- - -# PURPOSE AND OBJECTIVES - -The purpose of this document is to describe the development environment installation of OHIF viewer/Lesion Tracker on Linux Ubuntu. - -# DEVELOPMENT ENVIRONMENT INSTALLATION - -## Docker - -Docker is an open source software platform to create, deploy and manage virtualized application containers on common operating systems, with several allied tools. - -To install Docker, follow the instructions below: - -1. Open a terminal, and update the apt package index, by running the following command: - -sudo apt-get update - -Expected result: - - - -2. Install packages to allow apt to use a repository over HTTPS, by running the following command: - -sudo apt-get install apt-transport-https ca-certificates curl software-properties-common - -Expected result: - - - -3. Add Docker’s official GPG key, by running the following command: - -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - -Expected result: - - - -4. Verify that you now have the key with the fingerprint **9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88**, by searching for the last 8 characters of the fingerprint after running the following command: - -sudo apt-key fingerprint 0EBFCD88 - -Expected result: - - - -5. Use the following command to set up the stable repository. - -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - -Expected result: - - - -6. Install the latest version of Docker CE, by running the following command: - -sudo apt-get install docker-ce - -Expected result: - - - -7. Verify that Docker CE is installed correctly by running the hello-world image: - -sudo docker run hello-world - -Expected result: - - - -**Note**: Additional information about Docker installation on Linux Ubuntu OS can be foud at [https://docs.docker.com/install/linux/docker-ce/ubuntu/](https://docs.docker.com/install/linux/docker-ce/ubuntu/). - -## DCM4CHE - -Dcm4che ([https://www.dcm4che.org/](https://www.dcm4che.org/)) is a collection of open source applications and utilities for healthcare. - -To install Dcm4che, follow the instructions below: - -1. Before start, you need to create the following folders on your *Home *directory: - -* DCM4CHEE - -* dcm4chee-arc - -* db - -* ldap - -* slapd.d - -* storage - -* wildfly - - - -2. Create the following files on the DCM4CHEE folder, with the correspondent contents: - -docker-compose.env - -``` -STORAGE_DIR=/storage/fs1 -POSTGRES_DB=pacsdb -POSTGRES_USER=pacs -POSTGRES_PASSWORD=pacs -``` - - -docker-compose.yml - -``` -version: "3" -services: -ldap: -image: dcm4che/slapd-dcm4chee:2.4.44-14.1 -logging: -driver: json-file -options: -max-size: "10m" -ports: -- "389:389" -env_file: docker-compose.env -volumes: -- /etc/localtime:/etc/localtime:ro -- /etc/timezone:/etc/timezone:ro -- ~/dcm4chee-arc/ldap:/var/lib/ldap -- ~/dcm4chee-arc/slapd.d:/etc/ldap/slapd.d -db: -image: dcm4che/postgres-dcm4chee:10.4-14 -logging: -driver: json-file -options: -max-size: "10m" -ports: -- "5432:5432" -env_file: docker-compose.env -volumes: -- /etc/localtime:/etc/localtime:ro -- /etc/timezone:/etc/timezone:ro -- ~/dcm4chee-arc/db:/var/lib/postgresql/data -arc: -image: dcm4che/dcm4chee-arc-psql:5.14.1 -logging: -driver: json-file -options: -max-size: "10m" -ports: -- "8080:8080" -- "8443:8443" -- "9990:9990" -- "11112:11112" -- "2575:2575" -env_file: docker-compose.env -environment: -WILDFLY_CHOWN: /opt/wildfly/standalone /storage -WILDFLY_WAIT_FOR: ldap:389 db:5432 -depends_on: -- ldap -- db -volumes: -- /etc/localtime:/etc/localtime:ro -- /etc/timezone:/etc/timezone:ro -- ~/dcm4chee-arc/wildfly:/opt/wildfly/standalone -- ~/dcm4chee-arc/storage:/storage -``` - -Files/Folder will become like the image above: - - - -1. Open a terminal inside DCM4CHE folder, and run the following command: - -sudo snap install docker - -Expected result: - - - -4. Still inside the DCM4CHE folder, run the following command: - -sudo docker-compose -p dcm4chee up -d - -Expected result: - - - -1. The following commands can be used to control DCM4CHE: - -Stop all 3 containers: docker-compose -p dcm4chee stop - -Start all 3 containers again: docker-compose -p dcm4chee start - -Stop and delete all 3 containers: docker-compose -p dcm4chee down - -2. Now it is necessary to import some DICOM studies to DCM4CHE. Before importing these studies, you will need to download them. To do so, access the address [http://34.224.187.57:3000/studylist](http://34.224.187.57:3000/studylist), right click one or more studies, and select the option Export: - - - -3. Confirm the study export on the modal window that will appear: - - - -4. The study(ies) export will begin. You can follow the exporting progress: - - - -5. After the exporting progress, the study(ies) will be downloaded in a file called "studies.zip". Create a folder named “Studies” on your Home folder, and extract this and any other studies you download in this folder: - - - -6. Open the terminal, go to the DCM4CHE folder, and run the following command in order to send the studies to DCM4CHE: - -docker run -v ~/Studies/:/tmp --rm --network=dcm4chee_default dcm4che/dcm4che-tools:5.14.0 storescu -cDCM4CHEE@arc:11112 /tmp - -Expected result: - - - -7. Now, go to your browser and access the URL [http://localhost:8080/dcm4chee-arc/ui2/](http://localhost:8080/dcm4chee-arc/ui2/). Once opened, click on the refresh icon on the most right: - - - -8. The studies received by DCM4CHE will be shown: - - - -9. Open the terminal, go to your Home folder, and run the following command in order to clone viewers repository to your local: - -git clone https://github.com/OHIF/Viewers.git - -Expected result: - - - -10. Then, enter the Viewers folder, and run the following command in order to make sure that you are in master branch: - -git checkout master - -Expected result: - - - -11. Now go to OHIFViewer folder and run the following command: - -meteor npm install - -Expected result: - - - -12. Still on the OHIFViewer folder, run the following command: - -./bin/dcm4cheeDICOMWeb.sh - -Expected result: - - - -13. Now, go to your browser and access the URL [http://localhost:3000](http://localhost:3000). Once opened, change the Study Date filter to start on the year 2000: - - - -14. The imported studies will be show. Double click a study to open it: - - - -## Meteor - -Meteor is a JavaScript web framework that allows for rapid prototyping and produces cross-platform code. - -To install Meteor, follow the instructions below: - -**_Remark_***: This guide covers the Linux Ubuntu version. Installation instructions can be different on other operating systems.* - -1. Open a terminal, and run the following command: - -curl https://install.meteor.com/ | sh - -Expected result: - - - -## Starting OHIF Viewer after system restart - -These are the steps to run OHIFVIewer after system restart:. - -1. Open terminal - -2. Access the DCM4CHEE folder, on your Home folder - -3. Run the following command in order to start docker containers: - -docker-compose -p dcm4chee start - -4. Access the Viewers/OHIFViewer folder, on your Home folder - -5. Run the following command in order to run OHIF Viewer: - -./bin/dcm4cheeDICOMWeb.sh - -6. Access the address [http://localhost:3000](http://localhost:3000) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/usage.md b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/usage.md deleted file mode 100644 index cf5932ff9..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/usage.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Standalone Viewer ---- - -# Standalone Viewer - -## Quick Start - -Install dependencies: - -```bash -meteor npm install -``` - -Run the application: - -```bash -METEOR_PACKAGE_DIRS="../../Packages" ROOT_URL=http://localhost:3000 meteor -``` - -Open your web browser and navigate to one of the following URLs to test the standalone viewer application: - -```bash -http://localhost:3000/testId -``` - -Or, to load DICOMs: -```bash -http://localhost:3000/testDICOMs -``` - -### To Build for the Client - -It is possible to build this standalone viewer to run as a client-only bundle of HTML, JavaScript, and CSS. - -1. First, install [meteor-build-client-fixed2](https://www.npmjs.com/package/meteor-build-client-fixed2). - - ````bash - sudo npm install -g meteor-build-client-fixed2 - ```` - -2. Next, build the client bundle into an output folder ("myOutputFolder") with a base URL ("localhost:3000"). In production, this would be the URL where the Viewer is available. - - ```` - METEOR_PACKAGE_DIRS="../../Packages" meteor-build-client-fixed2 ../myOutputFolder -u localhost:3000 --legacy - ```` - -3. Test the bundled client-side package locally. - - Note: You will need to have Python installed to run the test server for this case. It is not a typical simple HTTP server. The bundled script redirects all URLs following the base URL to index.html. It will then use the routes defined in your application to handle the URL parameters. - - In our case, this means it will request a JSON file at baseURL/api/[id parameter]. - - So if you navigate to http://localhost:3000/sampleJPEG.json the application will retrieve the JSON from http://localhost:3000/api/sampleJPEG.json and use it to populate the viewer. If something appears to be broken, make sure you retrieve a JSON file at the /api URL. - - Create the api folder for your data - - ````bash - cd myOutputFolder - mkdir api - ```` - - Copy your data into the folder - - ````bash - cp ../etc/sample* api/ - ```` - - Run the server - - ```` bash - python ../etc/redirectingSimpleServer.py - ```` - - Open your web browser and navigate to http://localhost:3000/sampleJPEG.json or http://localhost:3000/sampleDICOM.json - - ## Authorization Header - - A ```token``` fragment parameter can be specified. If present this value will be used for http bearer authorization when making requests for the above JSON, and when retrieving images using the Cornerstone Image Loaders. - - Example : - - ``` - http://localhost:3000/sampleDICOM.json#token=1a2b3c4d - ``` - - -### Testing the Sample client-only build -For the sake of simplicity we have also included a pre-built client-only version of the standalone viewer, which can be found in the SampleClientOnlyBuild folder. - -You can test this with: - - ```` bash - cd SampleClientOnlyBuild - python ../etc/redirectingSimpleServer.py - ```` diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/README.md b/platform/docs/versioned_docs/version-1.0-deprecated/README.md deleted file mode 100644 index b7fe4256a..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/README.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: Introduction -slug: / -sidebar_position: 1 ---- - -##### Looking for your Deploy Preview? - Deploy Preview for Viewer - -The [Open Health Imaging Foundation](https://www.ohif.org) is developing an open -source framework for constructing web-based medical imaging applications. The -application framework is built using modern HTML / CSS / JavaScript and uses -[Cornerstone](https://cornerstonejs.org/) at its core to display and manipulate -medical images. It is built with Meteor, a Node.js-based full-stack JavaScript -platform. - -This documentation concerns the OHIF framework itself and its three example -applications: the OHIF Viewer, Lesion Tracker, and the Standalone Viewer. - -## The **OHIF Viewer**: A general purpose DICOM Viewer ([demo](http://viewer.ohif.org/)) - -![OHIF Viewer Screenshot](./assets/img/viewer.png) - -The Open Health Imaging Foundation intends to provide a simple general purpose -DICOM Viewer which can be easily extended for specific uses. The primary purpose -of the OHIF Viewer is to serve as a testing ground for the underlying packages -and the [Cornerstone](https://cornerstonejs.org/) family of libraries. - -## **Lesion Tracker**: An oncology-focused imaging application ([demo](http://lesiontracker.ohif.org/)) - -![Lesion Tracker Screenshot](./assets/img/lesionTracker.png) - -The Lesion Tracker is designed to facilitate quantitative assessments of tumour -burden over time. It is similar in scope to the ePAD Imaging Platform -(https://epad.stanford.edu/), developed at Stanford Medicine. - -## Study List & DICOM Connectivity - -![Study List Screenshot](./assets/img/worklist.png) - -The solution provides a study list and other resources for connecting to PACS -and other Image Archives through standard communication approaches (DICOM Web, -DICOM Messages). - -## Standalone Viewer ([demo](https://ohif-viewer.s3-website.eu-central-1.amazonaws.com/?url=https://raw.githubusercontent.com/OHIF/Viewers/master/StandaloneViewer/etc/sampleDICOM.json)) - -The Standalone Viewer offers only the client-side portions of the OHIF Viewer -with the Study List pages removed. This single-page viewer can be hosted as a -static site (e.g. on Amazon S3, Azure Blob Storage, or Github Pages), and easily -integrated with existing back-end DICOM storage systems. Alternative -[Cornerstone](https://cornerstonejs.org/) Image Loaders can be included to allow -your viewer to support non-DICOM objects (e.g. PNG, JPEG). diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Assessment_Progress.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Assessment_Progress.png deleted file mode 100644 index 7d4021cf7..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Assessment_Progress.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Association_Dialog.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Association_Dialog.png deleted file mode 100644 index ec21eea3b..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Association_Dialog.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Audit_Trails.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Audit_Trails.png deleted file mode 100644 index 2296825b3..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Audit_Trails.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CINE.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CINE.png deleted file mode 100644 index 612898adb..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CINE.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Change_Password.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Change_Password.png deleted file mode 100644 index ac7f7cff2..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Change_Password.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CompareMode.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CompareMode.png deleted file mode 100644 index d46678e07..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CompareMode.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Comparison.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Comparison.png deleted file mode 100644 index c9c91b914..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Comparison.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Configuration_Menu.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Configuration_Menu.png deleted file mode 100644 index 8aaac07ac..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Configuration_Menu.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Conformance_Check.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Conformance_Check.png deleted file mode 100644 index 409ddf5a2..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Conformance_Check.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Download.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Download.png deleted file mode 100644 index 5ee99d709..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Download.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Ellipse.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Ellipse.png deleted file mode 100644 index cd21fe816..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Ellipse.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipH.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipH.png deleted file mode 100644 index e79adb120..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipH.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipV.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipV.png deleted file mode 100644 index 99404b9ae..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipV.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Forgot_Password.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Forgot_Password.png deleted file mode 100644 index 4114d898f..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Forgot_Password.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Generate_Report.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Generate_Report.png deleted file mode 100644 index b224172b2..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Generate_Report.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_HUD_Panel.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_HUD_Panel.png deleted file mode 100644 index ca8d6aa4b..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_HUD_Panel.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Image_Viewer.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Image_Viewer.png deleted file mode 100644 index 37d8a8201..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Image_Viewer.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_After_Prerequisites.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_After_Prerequisites.png deleted file mode 100644 index d0e808ea2..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_After_Prerequisites.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png deleted file mode 100644 index c5fb9c3f3..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Final.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Final.png deleted file mode 100644 index 720f532ea..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Final.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Finish.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Finish.png deleted file mode 100644 index 5656990eb..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Finish.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Initial.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Initial.png deleted file mode 100644 index 90457fa2b..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Initial.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Installation.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Installation.png deleted file mode 100644 index 78fa9d246..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Installation.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png deleted file mode 100644 index 2935db087..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_License_Aggrement.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_License_Aggrement.png deleted file mode 100644 index db2e2465e..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_License_Aggrement.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Prerequisites.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Prerequisites.png deleted file mode 100644 index b0fbe2ed4..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Prerequisites.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Select_Location.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Select_Location.png deleted file mode 100644 index 9d216e655..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Select_Location.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Services.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Services.png deleted file mode 100644 index cef807778..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Services.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Successful.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Successful.png deleted file mode 100644 index 15d9a9da9..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Successful.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Uninstall.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Uninstall.png deleted file mode 100644 index 4d9060f82..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Uninstall.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Invert.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Invert.png deleted file mode 100644 index e4f2e81d5..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Invert.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Key_Timepoints.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Key_Timepoints.png deleted file mode 100644 index 79b02e63a..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Key_Timepoints.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Keyboard_Shortcuts.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Keyboard_Shortcuts.png deleted file mode 100644 index ae00ff995..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Keyboard_Shortcuts.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Login.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Login.png deleted file mode 100644 index 58250b654..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Login.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Logout.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Logout.png deleted file mode 100644 index 04c488fa6..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Logout.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Magnify.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Magnify.png deleted file mode 100644 index c96db713d..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Magnify.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Measurements.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Measurements.png deleted file mode 100644 index a9f0ba537..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Measurements.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Need_Account.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Need_Account.png deleted file mode 100644 index 3493b4d6c..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Need_Account.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Select_Location.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Select_Location.png deleted file mode 100644 index 7eaa3e932..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Select_Location.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Tool.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Tool.png deleted file mode 100644 index 7c2951246..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Tool.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png deleted file mode 100644 index 13d38194b..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png deleted file mode 100644 index c2a7071e9..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Study.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Study.png deleted file mode 100644 index b302d28eb..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Study.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png deleted file mode 100644 index 3cbc0c96d..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Start_Upload.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Start_Upload.png deleted file mode 100644 index 2fa7850d6..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Start_Upload.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload.png deleted file mode 100644 index b6f34a910..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload_Result.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload_Result.png deleted file mode 100644 index b545b1c69..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload_Result.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Pan.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Pan.png deleted file mode 100644 index 161a4f17a..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Pan.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Quick_Switch_Tool.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Quick_Switch_Tool.png deleted file mode 100644 index 478369c0e..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Quick_Switch_Tool.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Remove_Associate.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Remove_Associate.png deleted file mode 100644 index 4d3f580c6..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Remove_Associate.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Report_PDF.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Report_PDF.png deleted file mode 100644 index cb65fd0c3..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Report_PDF.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Reset.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Reset.png deleted file mode 100644 index e1e94fed2..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Reset.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Response_Criteria.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Response_Criteria.png deleted file mode 100644 index 01662fe12..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Response_Criteria.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Rotate_Right.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Rotate_Right.png deleted file mode 100644 index 246d3862a..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Rotate_Right.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save1.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save1.png deleted file mode 100644 index 040617fd0..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save1.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save2.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save2.png deleted file mode 100644 index 615156f4c..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save2.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Scroll.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Scroll.png deleted file mode 100644 index f8e0e3e54..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Scroll.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Select_Associate.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Select_Associate.png deleted file mode 100644 index 8916b0876..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Select_Associate.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Server_Info.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Server_Info.png deleted file mode 100644 index 4944121aa..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Server_Info.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Multiple.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Multiple.png deleted file mode 100644 index 6a0f757f6..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Multiple.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Single.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Single.png deleted file mode 100644 index a9ee8eace..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Single.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Studies_Panel.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Studies_Panel.png deleted file mode 100644 index ad170a732..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Studies_Panel.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StudyList.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StudyList.png deleted file mode 100644 index 3f126c784..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StudyList.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target.png deleted file mode 100644 index 24fc066fc..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_CR.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_CR.png deleted file mode 100644 index f760cfaf7..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_CR.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Delete.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Delete.png deleted file mode 100644 index dafbadb5c..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Delete.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Label.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Label.png deleted file mode 100644 index 5c9a9fe30..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Label.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Rename.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Rename.png deleted file mode 100644 index 4d71bf308..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Rename.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_UN.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_UN.png deleted file mode 100644 index e311ea0c3..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_UN.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Temp_Tool.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Temp_Tool.png deleted file mode 100644 index 8f92b2ead..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Temp_Tool.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Themes.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Themes.png deleted file mode 100644 index 5ae104f43..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Themes.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Lesion.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Lesion.png deleted file mode 100644 index aa4a026ed..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Lesion.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Series_Details.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Series_Details.png deleted file mode 100644 index 4bac8b1ec..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Series_Details.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Study.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Study.png deleted file mode 100644 index dc546b153..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Study.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Viewer.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Viewer.png deleted file mode 100644 index effc7da90..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Viewer.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL.png deleted file mode 100644 index d703e8b20..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL_Presets.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL_Presets.png deleted file mode 100644 index 464912753..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL_Presets.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Zoom.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Zoom.png deleted file mode 100644 index bdca718f1..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Zoom.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/lesionTracker.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/lesionTracker.png deleted file mode 100644 index effc7da90..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/lesionTracker.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/viewer.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/viewer.png deleted file mode 100644 index 21eacd59a..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/viewer.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/worklist.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/worklist.png deleted file mode 100644 index 3f126c784..000000000 Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/worklist.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/_category_.json deleted file mode 100644 index 5192fa6ac..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Connecting to ImageArchives", - "position": 3 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dcm4chee-with-docker.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dcm4chee-with-docker.md deleted file mode 100644 index 73aec35f7..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dcm4chee-with-docker.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -sidebar_position: 4 ---- -# DCM4CHEE with Docker - -1. Install Docker (https://www.docker.com/) -2. Follow the DCM4CHEE Guidelines for Running on Docker. - - The easiest path is to use Docker-Compose which will start and stop multiple containers for you. There are excellent instructions provided by the DCM4CHEE team on the 'light archive' repository: - - https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker#use-docker-compose - - * Create docker-compose.yml and docker-compose.env files - * Start the containers: - - ```` bash - docker-compose start - ```` - - **Note:** If you are running this on Mac OSX you will probably need to change the default docker-compose.yml file slightly. Specifically, the paths that refer to /var/local/ will likely need to be changed to /opt/ - -3. Run the OHIF Viewer or Lesion Tracker using the dcm4cheeDIMSE.json configuration file - - ````bash - cd OHIFViewer - PACKAGE_DIRS="../Packages" meteor --settings ../config/dcm4cheeDIMSE.json - ```` - -## Web Service URLs from DCM4CHEE: -Original source here: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker#web-service-urls - - - Archive UI: `http://localhost:8080/dcm4chee-arc/ui` - if secured, login with - - Username | Password | Role - --- | --- | --- - `user` | `user` | `user` - `admin` | `admin` | `user` + `admin` - - Keycloak Administration Console: `http://localhost:8080/auth`, login with Username: `admin`, Password: `admin`. - - Wildfly Administration Console: `http://localhost:9990`, login with Username: `admin`, Password: `admin`. - - Kibana UI: `http://localhost:5601` - - DICOM QIDO-RS Base URL: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs` - - DICOM STOW-RS Base URL: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs` - - DICOM WADO-RS Base URL: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs` - - DICOM WADO-URI: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado` diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dicomweb.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dicomweb.md deleted file mode 100644 index 1166b2b43..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dicomweb.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 2 ---- -# DICOM Web - -[DICOMWeb](https://en.wikipedia.org/wiki/DICOMweb) refers to RESTful DICOM Services and is a recently standardized set of guidelines for exchanging medical images and imaging metadata over the internet. Not all archives fully support it yet, but it is gaining wider adoption. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dimse.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dimse.md deleted file mode 100644 index 22c5a1d70..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dimse.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 3 ---- -# DICOM Message Service Element - -DIMSE Stands for [DICOM Message Service Element](http://dicom.nema.org/medical/dicom/current/output/chtml/part07/chapter_7.html) and is the standard method through which DICOM archives communicate. We support this messaging standard for the retrieval of study, series, and instance metadata because it is widely support. For certain PACS systems, it also (currently) provides faster query results than DICOMWeb. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/google-cloud-healthcare.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/google-cloud-healthcare.md deleted file mode 100644 index da60a8cfb..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/google-cloud-healthcare.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -sidebar_position: 6 ---- -# Google Cloud Healthcare - -> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a powerful option for storing medical imaging data in the cloud. - -An alternative to deploying your own PACS is to use a software-as-a-service provider such as Google Cloud. The Cloud Healthcare API promises to be a scalable, secure, cost effective image storage solution for those willing to store their data in the cloud. It offers an [almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom) which requires tokens generated via the [OAuth 2.0 Sign In flow](https://developers.google.com/identity/sign-in/web/sign-in). Images can even be transcoded on the fly if this is desired. The Cloud Healthcare API is a very attractive option because it allows us to avoid deploying the Meteor server entirely. We can just deploy OHIF as a client-only static site application. - -## Setup a Google Cloud Healthcare Project - -1. Create a Google Cloud account -1. Create a project in Google Cloud -1. Enable the [Cloud Healthcare API](https://cloud.google.com/healthcare/) for your project. -1. (Optional): Create a Dataset and Data Store for storing your DICOM data -1. Enable the [Cloud Resource Manager API](https://cloud.google.com/resource-manager/) for your project. - - *Note:* If you are having trouble finding the APIs, use the search box at the top of the Cloud console. - -1. Go to APIs & Services > Credentials to create an OAuth Consent screen and fill in your application details. - - - Under Scopes for Google APIs, click "manually paste scopes". - - Add the following scopes: - - https://www.googleapis.com/auth/cloudplatformprojects.readonly - - https://www.googleapis.com/auth/cloud-healthcare - -1. Go to APIs & Services > Credentials to create a new set of credentials: - - Choose the "Web Application" type - - Set up an [OAuth 2.0 Client ID](https://support.google.com/cloud/answer/6158849?hl=en) - - - Add your domain (e.g. ```http://localhost:3000```) to Authorized JavaScript origins. - - Add your domain, plus `_oauth/google` (e.g. ```http://localhost:3000/_oauth/google```) to Authorized Redirect URIs. - - Save your Client ID for later. -1. (Optional): Enable Public Datasets that are being hosted by Google: https://cloud.google.com/healthcare/docs/resources/public-datasets/ - -## Run the viewer with your OAuth Client ID - -1. Open the `config/oidc-googleCloud.json` file and change `YOURCLIENTID` to your Client ID value. -1. Run the OHIF Viewer using the oidc-googleCloud.json configuration file - -````bash -cd OHIFViewer -METEOR_PACKAGE_DIRS="../Packages" meteor npm install -METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/oidc-googleCloud.json -```` - -## Running via Docker - -OHIF is also providing a Docker container which can connect to Google Cloud Healthcare with a Client ID which is provided at runtime. This is a very simple method to get up and running. Internally, the container is running [Nginx](https://nginx.org/) to serve the [Standalone Viewer](../OHIF-Viewer/usage.md). - -1. Install Docker (https://www.docker.com/) -1. Run the Docker container, providing a Client ID as an environment variable. Client IDs look like `xyz.apps.googleusercontent.com`. - -````bash -docker run --env CLIENT_ID=$CLIENT_ID --publish 3000:80 ohif/viewer-google-cloud:latest -```` - -## Building the ohif/viewer-google-cloud Docker Image - -The [ohif/viewer-google-cloud](https://cloud.docker.com/u/ohif/repository/docker/ohif/viewer-google-cloud) Docker image is built as follows. The Dockerfile and nginx.conf are in the `/dockersupport/viewer-google-cloud` folder. - -1. [Install Meteor](https://www.meteor.com/install) -1. Clone the repository -```bash -git clone https://github.com/OHIF/Viewers.git -cd Viewers -``` - -1. Install meteor-build-client-fixed2 so you can build the Standalone Viewer -```bash -npm install -g meteor-build-client-fixed2 -``` - -1. Build the Standalone client-only OHIF Viewer -```bash -cd OHIFViewer/ -METEOR_PACKAGE_DIRS="../Packages" meteor npm install -METEOR_PACKAGE_DIRS="../Packages" meteor-build-client-fixed2 ../dockersupport/viewer-google-cloud/build -s ../config/oidc.json -``` - -1. Build the Docker image -```bash -cd ../dockersupport/viewer-google-cloud -docker build -t ohif/viewer-google-cloud . -``` - -1. Run the Docker image using an OAuth Client ID -```bash -docker run --env CLIENT_ID={$someID}.apps.googleusercontent.com --publish 3000:80 ohif/viewer-google-cloud -``` diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/options.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/options.md deleted file mode 100644 index e9c29971b..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/options.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 1 ---- -# Connecting to Image Archives - -We support DIMSE and DICOMWeb. Which one to use is up to you and depends on your PACS system. DICOMWeb requires no setup on the PACS-side whatsoever, whereas DIMSE may require you to add the 'OHIFDCM' aeTitle to the known DICOM Modalities of your Archive. This is the case for Orthanc, for example (See https://github.com/OHIF/Viewers/wiki/Orthanc-with-DIMSE). diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/orthanc-with-docker.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/orthanc-with-docker.md deleted file mode 100644 index 07f9d2dea..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/orthanc-with-docker.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_position: 5 ---- -# Orthanc with Docker - -Depending on whether or not you want uploaded studies to persist in Orthanc after Docker has been closed, there are two different methods for starting the Docker image: - -## Temporary data storage -This command will start an instance of the jodogne/orthanc-plugins Docker image. *All data will be removed when the instance is stopped!* - -```` -docker run --rm -p 4242:4242 -p 8042:8042 jodogne/orthanc-plugins -```` - -## Persistent data storage -In order to allow your data to persist after the instance is stopped, you first need to create an image and attached data volume with Docker. The steps are as follows: - -1. Create a persistent data volume for Orthanc to use - - ```` - docker create --name sampledata -v /sampledata jodogne/orthanc-plugins - ```` - - **Note: On Windows, you need to use an absolute path for the data volume, like so:** - - ```` - docker create --name sampledata -v '//C/Users/erik/sampledata' jodogne/orthanc-plugins - ```` - -2. Run Orthanc from Docker with the data volume attached - - ```` - docker run --volumes-from sampledata -p 4242:4242 -p 8042:8042 jodogne/orthanc-plugins - ```` - -3. Upload your data and it will be persisted diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/contributing.md b/platform/docs/versioned_docs/version-1.0-deprecated/contributing.md deleted file mode 100644 index d0afb8973..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/contributing.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -id: contributing -sidebar_position: 2 ---- - -# Contributing - -### I would like to contribute code - how do I do this? - -Fork the repository, make your change and submit a pull request. - -### Any guidance on submitting changes? - -While we do appreciate code contributions, triaging and integrating contributed -code changes can be very time consuming. Please consider the following tips when -working on your pull requests: - -- Functionality is appropriate for the repository. Consider posting on the forum - if you are not sure. -- Code quality is acceptable. We don't have coding standards defined, but make - sure it passes ESLint and looks like the rest of the code in the repository. -- Quality of design is acceptable. This is a bit subjective so you should - consider posting on the forum for specific guidance. -- The scope of the pull request is not too large. Please consider separate pull - requests for each feature as big pull requests are very time consuming to - understand. - -We will provide feedback on your pull requests as soon as possible. Following -the tips above will help ensure your changes are reviewed. - -### Testing contribution pull requests - -OHIF uses [netlify](https://netlify.com) so that pull requests are autogenerated -and available for testing. - -For example, -[this url](https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json) -allows you to test -[pull request 237, the request that created this FAQ entry,](https://github.com/OHIF/Viewers/pull/237) -using data pulled from Amazon S3. - -Replacing the number 237 in the link below with your pull request number should -let you test it as well and you can use this link for discussions on github -without requiring reviewers to download and build your branch. - -``` -https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json -``` - -If you have made a documentation change, a link like this will let you preview -the gitbook generated by the pull request: - -``` -https://deploy-preview-237--ohif.netlify.com/contributing.html -``` diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/data/_category_.json deleted file mode 100644 index f216db497..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/data/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Data Organization", - "position": 4 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/data-hierarchy.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/data-hierarchy.md deleted file mode 100644 index ceaee45b9..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/data/data-hierarchy.md +++ /dev/null @@ -1,5 +0,0 @@ -# Data Hierarchy - -## Studies, Series, Instances, Frames - -## Display Sets diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/image-viewport.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/image-viewport.md deleted file mode 100644 index 9d480566c..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/data/image-viewport.md +++ /dev/null @@ -1,5 +0,0 @@ -# Image Viewport - -### Main Viewport Component - -The [imageViewerViewport](https://github.com/OHIF/Viewers/tree/master/Packages/ohif-viewerbase/client/components/viewer/imageViewerViewport) component diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/measurements-and-annotations.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/measurements-and-annotations.md deleted file mode 100644 index 574f11845..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/data/measurements-and-annotations.md +++ /dev/null @@ -1 +0,0 @@ -# Measurements and Annotations diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/tool-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/tool-management.md deleted file mode 100644 index 399b04c8b..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/data/tool-management.md +++ /dev/null @@ -1 +0,0 @@ -# Tool Management diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/deployment/_category_.json deleted file mode 100644 index 0b0fa4cc8..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Deployment", - "position": 7 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/building-for-production.md b/platform/docs/versioned_docs/version-1.0-deprecated/deployment/building-for-production.md deleted file mode 100644 index d09a49f46..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/building-for-production.md +++ /dev/null @@ -1,152 +0,0 @@ -# Building for Production - -**This tutorial considers the current folder as `OHIFViewer/`**. - -### Dependencies - -First, you need to have installed: - -- [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) -- [Meteor](https://www.meteor.com/) -- [MongoDB](https://www.mongodb.com/) - -**MongoDB** is actually **not required** if you have a **remote MongoDB**. If your database is **local** then you need to have it running. - -### Check your packages - -Inside of the viewer folder make sure NPM packages are updated for production: - -```bash -npm install --production -``` - -### Building - -There are two ways of building OHIF Viewer for a specific DICOM server: **Automatically** or **Manually**. - -For both, we use the `meteor build` (check it's [docs](https://guide.meteor.com/deployment.html#custom-deployment)) app and **Orthanc Dicom Web Server**. - -For `meteor build` it's necessary to inform an output folder `myOutputFolder`. -**Remember to change `myOutputFolder` to your folder location.** - -OHIF Viewer will be built using **Orthanc DICOM Web server** configuration file `../config/orthancDICOMWeb.json` to set `METEOR_SETTINGS` environment var. - -*To run with another server, just point to the corresponding `.json` located in `config` folder or create your own.* - -#### Build automatically - -After this step, go directly to [Prepare to run production build](#prepare-to-run-production-build). - -##### Non-windows users - -```bash -METEOR_PACKAGE_DIRS="../Packages" METEOR_SETTINGS=$(cat ../config/orthancDICOMWeb.json) meteor build --directory myOutputFolder -``` - -##### Windows users - -Since there is no `cat` command in Windows `cmd`, use Windows `PowerShell` (at least version 3.0) instead. - -In `PowerShell`, open a new shell as an `admin`: - -```bash -Start-Process powershell -Verb runAs -``` - -Then: - -**Remember to change `OHIFViewerFolderLocation` to OHIFViewer's folder location.** - - ```bash -cd OHIFViewerFolderLocation -$settings = Get-Content ..\config\orthancDICOMWeb.json -Raw -$settings = $settings -replace "`n","" -replace "`r","" -replace " ","" -[Environment]::SetEnvironmentVariable("METEOR_SETTINGS", $settings, "Machine") -SET METEOR_PACKAGE_DIRS="../Packages" -meteor build --directory myOutputFolder -``` - -#### Build manually - -OHIF Viewer will be built normally, but with no DICOM Server information, which needs to be added when running the build. This is described in [Manually adding DICOM Server to the Viewer](#manually-adding-dicom-server-to-the-viewer). - -##### Non-windows users - -```bash -METEOR_PACKAGE_DIRS="../Packages" meteor build --directory myOutputFolder -``` - -##### Windows users in `cmd` - -```bash -SET METEOR_PACKAGE_DIRS="../Packages" -meteor build --directory myOutputFolder -``` - -### Prepare to run production build - -If everything went ok, `meteor build` created a `bundle` folder inside `myOutputFolder`. - -**Remember to change `myOutputFolder` to your folder location.** - -Go to that folder: - -```bash -cd myOutputFolder/bundle -``` - -Now install the **NPM dependencies**: - -```bash -cd programs/server -npm install -``` - -### To run production build - -Go back to the `bundle` folder: - -```bash -cd ../.. -``` -or (**Remember to change `myOutputFolder` to your folder location.**): - -```bash -cd myOutputFolder/bundle -``` - -3 environment variables are set before running Node.js: -- `MONGO_URL`: is the url to MongoDB. If it's **local**, you need to have it running -- `ROOT_URL`: the hostname where you can access your Viewer in the browser -- `PORT`: the port the Viewer will run - -This way, the Viewer can be accessed in `http://localhost:3000`, with MongoDB running locally using 27017 port (it's default). - -##### Non-windows users - -```bash -MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://localhost PORT=3000 node main.js -``` - -##### Windows users in `cmd` - -```bash -SET MONGO_URL=mongodb://localhost:27017/myapp -SET ROOT_URL=http://localhost -SET PORT=3000 -node main.js -``` - -### Manually adding DICOM Server to the Viewer - -If DICOM Server was configured automatically during the building process, skip this step. - -Access the viewer `http://localhost:3000` and toggle the **Options** menu, located at top right corner. Select **Server Information** option. - -In **Server Information** dialog click on **Add a new server** button and fill the fields accordingly to the DICOM Server to be added. - -In case of doubts about any field in this dialog, use `config/orthancDICOMWeb.json` as reference. - -After filling the form, click on the **Save** button and the new server will be listed. Make sure to activate it by clicking on the left most button (a checkbox button) in the **Actions** column. - -Refresh the page and Viewer will be connected to the DICOM Server. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/security.md b/platform/docs/versioned_docs/version-1.0-deprecated/deployment/security.md deleted file mode 100644 index 8dbb2f9bc..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/security.md +++ /dev/null @@ -1 +0,0 @@ -# Security diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/_category_.json deleted file mode 100644 index fd00c612d..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Essentials", - "position": 2 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/architecture.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/architecture.md deleted file mode 100644 index c4a58d1bf..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/architecture.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -sidebar_position: 2 ---- -# Architecture - -## Meteor -[Meteor](https://www.meteor.com/) is built on top of [Node.js](https://nodejs.org/en/), adding reactive templates ([Blaze](https://guide.meteor.com/blaze.html)), a [publish/subscribe](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) mechanism (via [WebSockets](https://en.wikipedia.org/wiki/WebSocket) and the [Distributed Data Protocol](https://blog.meteor.com/introducing-ddp-6b40c6aff27d), and a fully integrated client-server [MongoDB](https://www.mongodb.com/) making it easy to create reactive data elements in the UI. - -## Structure of an OHIF Application -The OHIF Framework is built as a set of small Meteor packages which can be included as necessary in the final application. Since the logic and templates are largely pushed into packages, the actual application-specific code for each Viewer is relatively short. - -In brief, to create a new Viewer application, you need to: -* Define the existing pages and the routes which will lead to them -* Define the overall application layout -* Include child templates from the OHIF Packages into your application layout -* Specify desired template options as necessary (e.g. which tools should appear in the toolbar) - -User interface components for your application can be defined in any View layer supported by Meteor (Officially: [Angular](https://www.meteor.com/tutorials/angular/creating-an-app), [React](https://www.meteor.com/tutorials/react/creating-an-app), [Blaze](https://www.meteor.com/tutorials/blaze/creating-an-app), Unofficially: [Vue](https://github.com/meteor-vue/vue-meteor)). diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/configuration.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/configuration.md deleted file mode 100644 index e44d91fe3..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/configuration.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -sidebar_position: 3 ---- -# Configurations - -There are a few pre-defined settings files that you can find in [config](https://github.com/OHIF/Viewers/tree/master/config) folder. These are Meteor files and all settings can be accessed as showed on [Meteor Website](https://docs.meteor.com/api/core.html#Meteor-settings). UI settings are also available as OHIF.uiSettings. See the [schema](https://github.com/OHIF/Viewers/blob/131d64854cb2eceff056a15ccb12c34b9e2baaa7/Packages/ohif-servers/both/schema/servers.js) for more information. - -## Server settings - -* `dropCollections (boolean)`: the server will drop all Mongo collections when it's set to `true` as soon as it has finished starting. This is useful for demo and development environments. - -## UI Settings - -* `studyListDateFilterNumDays (integer)`: define the default date filter (range) on study list. If it's 02/15/2017 and this config is set to 5 then it will search for studies between `02/10/2017` and `02/15/2017`. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/installation.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/installation.md deleted file mode 100644 index 2423c793a..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/installation.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -sidebar_position: 1 ---- -# Installation - -## Getting the Code - -Either clone the repository using Git: - -````bash -git clone git@github.com:OHIF/Viewers.git -```` - -or [Download the latest Master as a ZIP File](https://github.com/OHIF/Viewers/archive/master.zip). - -## Set up a DICOM server - -1. Choose and install an Image Archive -2. Upload some data into your archive (e.g. with DCMTK's [storescu](http://support.dcmtk.org/docs/storescu.html) or your archive's web interface) -3. Keep the server running - -#### Open Source DICOM Image Archive Options - - - -| Archive | Installation | -| ----------- | ----------- | -| [DCM4CHEE Archive 5.x](https://github.com/dcm4che/dcm4chee-arc-light) | [Installation with Docker](https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker) | -| [Orthanc](https://www.orthanc-server.com/) | [Installation with Docker](http://book.orthanc-server.com/users/docker.html) | -| [DICOMcloud](https://github.com/DICOMcloud/DICOMcloud) (**DICOM Web only**) | [Installation](https://github.com/DICOMcloud/DICOMcloud#running-the-code) | -| [OsiriX](http://www.osirix-viewer.com/) (**Mac OSX only**) | Text | -| [Horos](https://www.horosproject.org/) (**Mac OSX only**) | Text | - - - - - -*Feel free to make a Pull Request if you want to add to this list.* - -## Set up and test the OHIF Viewer (or LesionTracker) application: -1. [Install Meteor](https://www.meteor.com/install) -2. Open a new terminal tab in one of the Application directories (OHIFViewer or LesionTracker) -3. Instruct Meteor to install all dependent NPM Packages - - ````bash - METEOR_PACKAGE_DIRS="../Packages" meteor npm install - ```` - -4. Run Meteor using one of the available configuration files. - - ````bash - METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/orthancDICOMWeb.json - ```` - - **Note:** On Windows, you may need to set PACKAGE_DIRS="../Packages" in your Environment Variables in your operating system settings. - -5. Launch the OHIF Viewer / Lesion Tracker Study List by visiting [http://localhost:3000/](http://localhost:3000/) in a web browser. - - **If everything is working correctly, you should see the Study List from your archive when you visit the Study List.** - -6. Double-click on a Study in the Study List to launch it in the Viewer - - **If everything is working correctly, you should see your study load into the Viewer.** - -#### Troubleshooting -* If you receive a *"No Studies Found"* message and do not see your studies, try changing the Study Date filters to a wider range. -* If you see any errors in your server console, check the Troubleshooting page for more in depth advice. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/meteor-packages.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/meteor-packages.md deleted file mode 100644 index a9fd4d26b..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/meteor-packages.md +++ /dev/null @@ -1,60 +0,0 @@ -# Meteor Packages - -### Commands (*ohif-commands*) -### Core (*ohif-core*) - -## Cornerstone Package (*ohif-cornerstone*) - -This package contains a number of front-end libraries that help us build web-based medical imaging applications. - -These are: -- [dicomParser](https://github.com/cornerstonejs/dicomParser): -A lightweight JavaScript library for parsing DICOM P10 byte streams in modern web browsers (IE10+), Node.js, and Meteor. - -- [Cornerstone Core](https://github.com/cornerstonejs/cornerstone): -A lightweight JavaScript library for displaying medical images in modern web browsers that support the HTML5 canvas element. - -- [Cornerstone Tools](https://github.com/cornerstonejs/cornerstoneTools): -A library built on top of cornerstone that provides a set of common tools needed in medical imaging to work with images and stacks of images - -- [Cornerstone Math](https://github.com/cornerstonejs/cornerstoneMath): -Math and computational geometry functionality for Cornerstone - -- [Cornerstone WADO Image Loader](https://github.com/cornerstonejs/cornerstoneWADOImageLoader): -A Cornerstone Image Loader for DICOM P10 instances over HTTP. This can be used to integrate cornerstone with WADO-URI servers or any other HTTP based server that returns DICOM P10 instances (e.g. Orthanc or custom servers). - -- [Hammer.js](https://github.com/hammerjs/hammer.js): -A JavaScript library for multi-touch gestures - -### Design (*ohif-design*) -### DICOM Services (*ohif-dicom-services*) -It contains a number of helper functions for retrieving common value types (e.g. JSON, patient name, image frame) from a DICOM image. This package is for server-side usage. - -### Hanging Protocols (*ohif-hanging-protocols*) -### Header (*ohif-header*) -### Hotkeys (*ohif-hotkeys*) - -### Lesion Tracker (*ohif-lesiontracker*) -This package stores all of the oncology-specific tools and functions developed for the Lesion Tracker application. Here we store, for example, the Target measurement and Non-target pointer tools that are used to monitor tumour burden over time. - -This package also stores Meteor components for the interactive lesion table used in the Lesion Tracker, and dialog boxes for the callbacks attached to the Target and Non-target tools. - -### Logging (*ohif-log*) -### Logging (*ohif-measurements*) -### Metadata (*ohif-metadata*) -### Polyfilling Functionality (*ohif-polyfill*) - -### Select Tree UI (*ohif-select-tree*) -### Server Settings UI (*ohif-servers*) -### Studies (*ohif-studies*) -### Study List UI (*ohif-study-list*) -### Common Themes (*ohif-themes-common*) -### Theming (*ohif-themes*) -### User Management (*ohif-user-management*) -### User (*ohif-user*) - -### Basic Viewer Components (*ohif-viewerbase*) -This is the largest package in the repository. It holds a large number of reusable Meteor components that are used to build both the OHIF Viewer and Lesion Tracker. - -### WADO Proxy (*ohif-wadoproxy*) -Proxy for CORS diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/troubleshooting.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/troubleshooting.md deleted file mode 100644 index ca2e47f5f..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/troubleshooting.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -sidebar_position: 4 ---- -# Troubleshooting - -## Common Problems - -| Problem | Most Common Reasons | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Can't retrieve Study List over DICOMWeb | 1. QIDO root URL is incorrect 2. DICOM Web is not enabled on PACS | -| Can't retrieve Study List over DIMSE | 1. PACS is not configured to allow connections from OHIF Meteor Server | -| Can't retrieve images | 1. WADO Root URL is incorrect 2. DICOM Web is not enabled on PACS 3. HTTP Basic Authentication username and password are incorrect or not provided. | - - - -## Debugging Steps -### Can't retrieve Study List over DICOMWeb - -1. Check that you can query your PACS using an alternative DICOM Web client (e.g. cURL, or a Web Browser). If you cannot, then your PACS is configured incorrectly. Refer to the documentation of the image archive. -2. - - -### Can't retrieve Study List over DIMSE - -### Can't retrieve images diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/_category_.json deleted file mode 100644 index c845acf43..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Example Applications", - "position": 9 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/lesion-tracker.md b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/lesion-tracker.md deleted file mode 100644 index 11fe6b818..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/lesion-tracker.md +++ /dev/null @@ -1 +0,0 @@ -# Lesion Tracker diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/ohif-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/ohif-viewer.md deleted file mode 100644 index e6c0db4cb..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/ohif-viewer.md +++ /dev/null @@ -1 +0,0 @@ -# OHIF Viewer diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/standalone-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/standalone-viewer.md deleted file mode 100644 index 2ff802c4f..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/standalone-viewer.md +++ /dev/null @@ -1 +0,0 @@ -# Standalone Viewer diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/faq/general.md b/platform/docs/versioned_docs/version-1.0-deprecated/faq/general.md deleted file mode 100644 index 90c7f1994..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/faq/general.md +++ /dev/null @@ -1,88 +0,0 @@ -# General - -### How do I file a bug? - -We accept and triage bug reports through Github primarily. - -1. [Create a Github account](https://github.com/join) -2. Search the current [Issue List](https://github.com/OHIF/Viewers/issues) to - ensure you are not creating a duplicate issue. - -If your issue already exists, post a comment to show us that this issue also -affects you. - -3. If no prior issue exists, - [Create a New Issue](https://github.com/OHIF/Viewers/issues/new) on the - repository. - -Some tips for filing a new issue: - -- **Make sure your issue is reproducible**: If we try to reproduce your issue - given your provided steps and we cannot reproduce it, we will not be able to - fix it. _Nobody wants to spend time guessing how to reproduce your issue!_ - Before filing, please reproduce your issue more than once and clearly describe - the steps taken. -- **If you are reporting a user interface issue, provide screenshots**: A - picture is worth a thousand words. If your issue concerns the UI, screenshots - will help us identify the issue dramatically faster since it can be extremely - challenging to describe UI bugs with text. _You should still clearly describe - the steps that you took to produce the issue_. -- **Include platform & environment**: Your operating system, web browser, and - web browser version are highly relevant for many bugs. Please provide these - with all bug reports. -- **Include expected and actual result**: Tell us what you expected to happen, - and what actually happened. If you don't do this, we might not consider it a - bug. - -### How can I request a new feature? - -At the moment we are in the process of defining our roadmap and will do our best -to communicate this to the community. If your requested feature is on the -roadmap, then it will most likely be built at some point. If it is not, you are -welcome to build it yourself and [contribute it](../contributing.md). If you -have resources and would like to fund the development of a feature, please -[contact us](https://www.ohif.org). - -### Who should I contact about Academic Collaborations? - -[Gordon J. Harris](http://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/) -at Massachusetts General Hospital is the primary contact for any academic -collaborators. We are always happy to hear about new groups interested in using -the OHIF framework, and may be able to provide development support if the -proposed collaboration has an impact on cancer research. - -### Do you offer commercial support? - -The Open Health Imaging Foundation does not offer commercial support, however, -some community members do offer consulting services. You can search our -[Community Forum](https://community.ohif.org/) for more information. - -### I emailed my question to you directly and you did not respond. Why not? - -Emailing developers directly is not a shortcut to faster support. Please file -your issues and questions on Github so that everyone can benefit from the -discussion and solutions. - -### Do your Viewers have [510(k) Clearance](https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/) from the U.S. F.D.A or [CE Marking](https://ec.europa.eu/growth/single-market/ce-marking_en) from the European Commission? - -**NO.** The OHIF Viewer, Lesion Tracker, and Standalone Viewer, **NOT** F.D.A. -cleared or CE Marked. It is the users responsibility to ensure compliance with -applicable rules and regulations. The -[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF -Framework does not prevent your company or group from seeking F.D.A. clearance -for a product built using the framework. - -If you have gone this route (or are going there), please let us know because we -would be interested to hear about your experience. - -### Are your Viewers [HIPAA](https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act) Compliant? - -**NO.** The OHIF Viewer, Lesion Tracker, and Standalone Viewer **DO NOT** -fulfill all of the criteria to become HIPAA Compliant. It is the users -responsibility to ensure compliance with applicable rules and regulations. - -The Lesion Tracker application demonstrates some available components and -features (e.g. [Audit Trail](../lesion-tracker/audit-trail.md), automatic -logoff, and unique user identification for -[User Accounts](../lesion-tracker/user-accounts.md)) which could be used by -third parties seeking HIPAA compliance. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/faq/technical.md b/platform/docs/versioned_docs/version-1.0-deprecated/faq/technical.md deleted file mode 100644 index f7cc314f2..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/faq/technical.md +++ /dev/null @@ -1,8 +0,0 @@ -# Technical -## Why is your framework built with Meteor? - -At the time of project conception, Meteor was a simple way to begin using bleeding edge web technologies without putting significant effort into configuration and build processes. Although Meteor does have some drawbacks, it remains a simple all-in-one solution. - -## Do you have any plans to stop using Meteor? - -We have considered migrating templates from Blaze (http://blazejs.org/) to React (https://reactjs.org/) or Vue (https://vuejs.org/), simply because these decouple the view layer from the remainder of the application. Blaze currently lacks [Stand-alone Support](http://blazejs.org/#Better-Stand-alone-Support) which means that our templates are not reusable outside of a Meteor application. This is certainly a downside, but the resource cost to migrate every template is significant. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/layout/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/layout/_category_.json deleted file mode 100644 index 1da2602ba..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/layout/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Viewport Layout", - "position": 5 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/layout/hanging-protocols.md b/platform/docs/versioned_docs/version-1.0-deprecated/layout/hanging-protocols.md deleted file mode 100644 index 319bda35f..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/layout/hanging-protocols.md +++ /dev/null @@ -1 +0,0 @@ -# Hanging Protocols diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/layout/layout-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/layout/layout-management.md deleted file mode 100644 index 3756edd1c..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/layout/layout-management.md +++ /dev/null @@ -1 +0,0 @@ -# Layout Management diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/_category_.json deleted file mode 100644 index 1f5374a5c..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Lesion Tracker", - "position": 11 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/audit-trail.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/audit-trail.md deleted file mode 100644 index 09726ddaa..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/audit-trail.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -sidebar_position: 9 ---- -# Audit Trail - -1. To view Audit Trails, select **View Audit Log** on the Configuration menu. -2. To filter audit logs for a column or multiple columns, type or select in the - related column or columns. - -![Audit Trails](../assets/img/LesionTracker/LT_Audit_Trails.png) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/installation-on-windows.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/installation-on-windows.md deleted file mode 100644 index 03d07a021..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/installation-on-windows.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Installation on Windows - -On Windows, the easiest way to install LesionTracker is using the provided -Windows Installer. You can download the latest version of LesionTracker -installer from [OHIF](https://ohif.org) - -## Install - -1. After you have obtained the latest version of LesionTracker installer, - double-click the installer to install the application. -2. When you start the installer, setup wizard will pop up. - -![Installer Initial](../assets/img/LesionTracker/LT_Installer_Initial.png) - -3. Click **Install** to start the installation. -4. LesionTracker will install prerequisites if they do not exist before - installing the application. - -![Prerequisites](../assets/img/LesionTracker/LT_Installer_Prerequisites.png) - -5. Click the **Next** if you accept the installation. - -![After Prerequisites](../assets/img/LesionTracker/LT_Installer_After_Prerequisites.png) - -6. Read the license agreement, and if you agree, click **I accept the terms in - the License Agreement** and then click the **Next**. - -![License Aggrement](../assets/img/LesionTracker/LT_Installer_License_Aggrement.png) - -7. Select a location to install LesionTracker and click the **Next** to - continue. - -![Select Location](../assets/img/LesionTracker/LT_Installer_Select_Location.png) - -8. Click the **Install** to start the installation and the installation will be - started. - -![Launch Installation](../assets/img/LesionTracker/LT_Installer_Launch_Installation.png) - -9. Click **Finish** to complete the installation. - -![Finish](../assets/img/LesionTracker/LT_Installer_Finish.png) - -10. When the installation is done, - -- You will see the final dialog which gives the URLs in order to connect to - LesionTracker Viewer and Orthanc Server. - - - It will launch computer's default browser and display LesionTracker Viewer - if you click 'http://localhost:3000' - - It will launch computer's default browser and enables to upload studies to - Orthanc Server if you click 'http://localhost:8042' - - ![Final](../assets/img/LesionTracker/LT_Installer_Final.png) - -- Also, LesionTracker installer will create two shortcuts named LesionTracker - and Orthanc Server on the Desktop. - - - It will launch computer's default browser and display LesionTracker image - viewer if you click LesionTracker application on the Desktop. - - It will launch computer's default browser and enables to upload studies to - Orthanc Server if you click Orthanc Server application. - - ![Desktop Shortcuts](../assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png) - -11. Click the **Done** and **Close** to close the installation window. - -![Installation Successful](../assets/img/LesionTracker/LT_Installer_Successful.png) - -12. After the installation is completed, - -- You can find the project folder in C:\Program Files\OHIF\LesionTracker if you - use the default installation location. -- You will find 4 program executables which eases managing the services in the - project folder: - - - **Start Services.exe:** Starts LesionTracker, MongoDB and Orthanc services - manually. - - **Stop Services.exe:** Stops LesionTracker, MongoDB and Orthanc services - manually. - - **Turn Off Auto Services.exe:** Configures not to start LesionTracker, - MongoDB and Orthanc services automatically when Windows starts up. - - **Turn On Auto Services.exe:** Configures to start LesionTracker, MongoDB - and Orthanc services automatically when Windows starts up. - - ![Services](../assets/img/LesionTracker/LT_Installer_Services.png) - -## Uninstall - -1. Open the Start Menu. -2. Click **Settings**. -3. Click **Apps** on the Settings menu. -4. Select **Apps & features** from the left panel. -5. Find the LesionTracker in programs list and click LesionTracker, then click - the **Uninstall**. - -![Uninstall](../assets/img/LesionTracker/LT_Installer_Uninstall.png) - -6. Click **Uninstall** from the LesionTracker Setup dialog. - -![Launch Uninstall](../assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png) - -When the uninstallation is done, the uninstallation will remove; - -- Prerequisites: MongoDB, nodejs and Orthanc Server -- The project folder, OHIF, from C:\Program Files -- Background Services: LesionTracker Server, MongoDB and Orthanc services -- Shortcuts: LesionTracker and Orthanc Server shortcuts on the Desktop diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/manage-studies-in-orthanc.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/manage-studies-in-orthanc.md deleted file mode 100644 index 2bc8f01fe..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/manage-studies-in-orthanc.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -sidebar_position: 3 ---- -# Manage Studies in Orthanc -Orthanc is an open-source, simple and powerful standalone DICOM server which eases DICOM scripting and data management for clinical routine and medical research. LesionTracker installer installs Orthanc as a service and runs automatically in the background. Orthanc serves a web interface, Orthanc Explorer, and it can be accessed through http://localhost:8042 in your web browser or the Orthanc Server shortcut which is created by LesionTracker installer and loads http://localhost:8042 in your default browser. - -## Upload a study into Orthanc using Orthanc Explorer - -1. Click **Upload** button at the top-right of Orthanc Explorer and you will be brought to the upload page. - - ![Upload](../assets/img/LesionTracker/LT_Orthanc_Upload.png) - -2. Select the DICOM files and drag those files into Orthanc Explorer. - - ![Drag and Drop Studies](../assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png) - -3. Click **Start the upload** and wait until the upload is complete. - - ![Start the Upload](../assets/img/LesionTracker/LT_Orthanc_Start_Upload.png) - -4. Click **Patients** to view the uploaded study. - - ![Upload Result](../assets/img/LesionTracker/LT_Orthanc_Upload_Result.png) - -## Delete a study from Orthanc using Orthanc Explorer - -1. Select a patient from Patients list. - - ![Select Patient](../assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png) - -2. Select a study of the patient. - - ![Select Study](../assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png) - -3. Select **Delete this study** in the Interact menu. - - ![Delete Study](../assets/img/LesionTracker/LT_Orthanc_Delete_Study.png) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/server-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/server-management.md deleted file mode 100644 index 1389a6afb..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/server-management.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -sidebar_position: 8 ---- -# Server Management - -LesionTracker supports configuration of multiple servers and allows to switch between them. To configure the servers, click **Server Information** on the Configuration Menu. - -![Server Information](../assets/img/LesionTracker/LT_Server_Info.png) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/study-and-timepoint-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/study-and-timepoint-management.md deleted file mode 100644 index 5a0edc457..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/study-and-timepoint-management.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -sidebar_position: 5 ---- -# Study and Timepoint Management - -![Study List](../assets/img/LesionTracker/LT_StudyList.png) - -1. Study List allows to sort each column. To sort studies, click the desired - column header. -2. Study List is filterable for each column and you can filter by more than one - column by typing in the column field. To filter a column, type the text in - the desired column header field and press Enter. -3. Pagination is provided for the Study List at the bottom of the page. You can - change the number of rows per page or the current page. - -## Study List Context Menu - -When you right-click on the study row, Study Context Menu will pop up. Study -Context Menu includes operations at the study level. - -There are a couple of ways to view a study: - -- Simply double-click on the study row. -- Select **View** option on the Study Context Menu, then you will be redirected - to Viewer page. - -![View Study](../assets/img/LesionTracker/LT_View_Study.png) - -### Timepoint Association - -1. To link a study with a timepoint, right-click on the study row and select - **Associate** option on the Study Context Menu. You will see Study - Association dialog. -2. To link multiple studies at the same time, hold Shift key and select studies - you would like to associate, then right-click and select Associate on the - Study Context Menu - -![Select Associate](../assets/img/LesionTracker/LT_Select_Associate.png) - -The Study Association dialog allows you to select and unselect Timepoint Type -for the selected study or studies. Also, it will include all studies within 14 -days of your selected studies, in case you forgot to select a study. After you -identify timepoints for the selected study or studies, click **Save** to save -the association. - -![Association Dialog](../assets/img/LesionTracker/LT_Association_Dialog.png) - -### Remove Timepoint Association - -To unlink a timepoint association with a study, right-click on the study row and -select **Remove Association** option on the Study Context Menu. Remove -Association will be disabled if the study is already dissociated. - -![Remove Associate](../assets/img/LesionTracker/LT_Remove_Associate.png) - -### View Series Details - -To view details of related series, right-click on the study row and select -**View Series Details** option on the Study Context Menu, then Series Details -dialog will pop up. Series Details dialog gives a summary of the selected study. - -![View Series Details](../assets/img/LesionTracker/LT_View_Series_Details.png) - -### Anonymize (Under Development) - -... - -### Send (Under Development) - -... - -### Export (Under Development) - -... - -### Delete (Under Development) - -... diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-accounts.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-accounts.md deleted file mode 100644 index 94bcd16b4..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-accounts.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -sidebar_position: 4 ---- -# User Accounts - -## Sign In - -LesionTracker requires authentication to access the viewer. -If you are already a registered user, enter Your Email and Password and click **Sign In** - -![Login](../assets/img/LesionTracker/LT_Login.png) - -## Register An Account - -If you are a new user, click **Need An Account** and fill the all fields and click **Join Now** - -![Need Account](../assets/img/LesionTracker/LT_Need_Account.png) - -Note: There is a Test Drive button on the top-right of Login page. Test Drive is used to skip registration process and allows to use LesionTracker with a test account. This will be removed in the next versions. - -## Forgot Password - -To reset the password, enter Your Email and click **Send Reminder**. An email that includes a link to reset your password will be sent to your email address. - -![Forgot Password](../assets/img/LesionTracker/LT_Forgot_Password.png) - -## Change Password - -To change password: - -1. Click Configuration menu icon at the top-right of the workspace then select **Change Password** -2. Enter your password and click **Change Password** - -![Change Password](../assets/img/LesionTracker/LT_Change_Password.png) - -## Logout - -To logout, click **Logout** on the Configuration menu. - -![Logout](../assets/img/LesionTracker/LT_Logout.png) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-preferences.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-preferences.md deleted file mode 100644 index bce9dd661..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-preferences.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -sidebar_position: 7 ---- -# User Preferences -To view User Preferences dialog, select **Preferences** on the Configuration menu. User Preferences dialog includes a couple of tabs: - -![Configuration Menu](../assets/img/LesionTracker/LT_Configuration_Menu.png) - -#### Keyboard Shortcuts -There are a number of default shortcuts for the defined tools. To change the shortcut for the tool, type the desired key into the tool field. - -![Keyboard Shortcuts](../assets/img/LesionTracker/LT_Keyboard_Shortcuts.png) - -#### Window Width & Level Presets -There are a number of default Window Width/Level settings in the Window W/L tab. To create a new setting, enter the desired column field. - -![Window W/L](../assets/img/LesionTracker/LT_WL_Presets.png) - -## Color Themes - -1. To modify the colors in the viewer, select **Themes** from Configuration menu. -2. Select a theme then click **Apply theme** to change the theme. - -![Themes](../assets/img/LesionTracker/LT_Themes.png) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/using-the-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/using-the-viewer.md deleted file mode 100644 index d24414527..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/using-the-viewer.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -sidebar_position: 6 ---- -# Using the Viewer - -After you double-click a study, the Lesion Tracking imaging application will be launched. The basic patient and study data is displayed in the Image Viewer. - -![Viewer](../assets/img/LesionTracker/LT_Viewer.png) - -### Switching Studies and Series -There are two methods to switch studies and series inside each viewport: - -#### Studies Panel - -Studies Panel at the left side of the workspace allows you to change studies and series. Click **Studies** button to toggle Studies Panel. You can view images in two ways: - -1. Double-click the desired series thumbnail. -2. Drag and drop the desired series thumbnail on the image viewer. - -![Studies Panel](../assets/img/LesionTracker/LT_Studies_Panel.png) - -#### Quick Switch Tool - -Quick Switch Tool in the top-middle of the workspace allows you to switch studies and series for the active image viewer easily. Selected series will be colored by the theme colors. - -![Quick Switch Tool](../assets/img/LesionTracker/LT_Quick_Switch_Tool.png) - -## Image Manipulation Tools -Image Manipulation Tools are available in the toolbar at the top of the workspace. - -### Zoom Tool - -There are a couple of ways to zoom the image: - -1. Select **Zoom Tool** and down on the image with the left mouse button. -2. Down on the image the right mouse button without selecting Zoom Tool. - -![Zoom](../assets/img/LesionTracker/LT_Studies_Panel.png) - -### Window Width & Level - -There are a couple of ways to change the window/level of the image: - -1. Select **Levels Tool** and drag on the image with the left mouse button. Dragging up decreases the level and dragging left decreases the window width. - - ![Window W/L](../assets/img/LesionTracker/LT_WL.png) - -2. Alternatively, you can use default Window/Level Presets which are defined in User Preferences dialog or create your window level values. - - ![Window W/L Presets](../assets/img/LesionTracker/LT_WL_Presets.png) - -### Pan Tool - -There are a couple of ways to pan the image: -1. Select **Pan Tool** and move the image around the screen with the left mouse button. -2. Move the image around the screen with the middle mouse button without selecting Pan Tool. - -![Pan Tool](../assets/img/LesionTracker/LT_Pan.png) - -### Stack Scroll Tool - -There are a number of ways to scroll images: -1. Scroll up and down the mouse wheel. -2. Drag the scroll bar at the right side of the image viewer. -3. Use Up and Down buttons on the keyboard. - -![Scroll Tool](../assets/img/LesionTracker/LT_Scroll.png) - -## Lesion Tracking Basics -### View Case Response Criteria - -LesionTracker supports Conformance Checks for RECIST 1.1 and click **Trial** to view Response Criteria details. - -_Note:_ Trial button is available if the study is associated with a timepoint. - -![Response Criteria](../assets/img/LesionTracker/LT_Response_Criteria.png) - -### Measurements Table - -To view measurements and manipulate the lesions, click **Measurements** button at the top-right of the workspace. The Measurements table will be displayed at the right side of the workspace. To hide the Measurements the table, click Measurements button. - -_Note:_ Measurements button is available if the study is associated with a timepoint. - -![Measurements](../assets/img/LesionTracker/LT_Measurements.png) - -### Target Tool - -1. To create a target, select **Target Tool** and click on the image. -2. Hold, drag and release the mouse button to create the target. - - ![Target Tool](../assets/img/LesionTracker/LT_Target.png) - -3. To label the target, click **Add Label** button. - - ![Target Label](../assets/img/LesionTracker/LT_Target_Label.png) - -4. To resize the measurement, hover the mouse over the end points of the target then click and drag. -5. To rename the lesion, click the lesion number in the Measurements table then click **Rename** - - ![Rename Target](../assets/img/LesionTracker/LT_Target_Rename.png) - -6. To delete the lesion, click the lesion number in the Measurements table then click **Delete**. It will delete the lesion from the Measurements table and the image. - - ![Delete Target](../assets/img/LesionTracker/LT_Target_Delete.png) - -#### Completely Resolved (CR) Target Tool -To make a lesion completely resolved, click Target Tool and select **CR Target** from Target Tool pop up. - -![CR Target Tool](../assets/img/LesionTracker/LT_Target_CR.png) - -#### Unknown (UN) Target Tool -To mark a lesion as unknown, click Target Tool and select ** UN Target** from Target Tool pop up. - -![UN Target Tool](../assets/img/LesionTracker/LT_Target_UN.png) - -### Non-Target Tool - -1. To create a non-target, select **Non-Target Tool** and click on the image. -2. Hold, drag and release the mouse button to create a non-target. - - ![Non-Target Tool](../assets/img/LesionTracker/LT_NonTarget_Tool.png) - -3. Select Lesion Location and Lesion Location Response then click **Confirm**. - - ![Non-Target Select Location](../assets/img/LesionTracker/LT_NonTarget_Select_Location.png) - -4. To resize the measurement, hover the mouse over the end points of the non-target then click and drag. -5. To rename the lesion, click the lesion number in the Measurements table then click **Rename** -6. To delete the lesion, click the lesion number in the Measurements table then click **Delete**. It will delete the lesion from the Measurements table and the image. - -### View Lesion - -To view the lesion on the image, click the measurement record in the Measurements table then the image which includes the lesion will be displayed by activating the selected lesion. - -![View Lesion](../assets/img/LesionTracker/LT_View_Lesion.png) - -### Conformance Checks - -Any lesion that will conflict with Response Criteria for RECIST 1.1 will be flagged in the Measurements table. To view criteria non-conformities, click the icon at the left of the lesion record in the Measurements table. - -![Conformance Check](../assets/img/LesionTracker/LT_Conformance_Check.png) - -### Generating PDF Reports - -1. To generate reports of measurements, click **Generate Report** button at the bottom of the Measurements table. - -![Generate PDF](../assets/img/LesionTracker/LT_Generate_Report.png) - -2. A PDF file which includes target and non-target measurements with annotated image snapshots will be created. - -![Report PDF](../assets/img/LesionTracker/LT_Report_PDF.png) - -### Saving Measurements - -There a couple of ways to save changes: -1. Click **Save** button at the top-right of the workspace. - - ![Save](../assets/img/LesionTracker/LT_Save1.png) - -2. Click **Study list** to save changes and go back to Study List. - - ![Save](../assets/img/LesionTracker/LT_Save2.png) - -### Timepoint Comparison - -LesionTracker allows to compare time points and manipulate the measurements on the timepoint images. To launch the compare mode: - -1. Associate a study with Follow-up timepoint then double-click Follow-up study. Follow-up and Baseline studies will be displayed in 1x2 layout and Baseline study will show the image which includes the first target measurement as default. -2. Measurements in the Follow-up must be created in the same order as they were on Baseline. - - ![Compare Mode](../assets/img/LesionTracker/LT_CompareMode.png) - -#### Comparison - -    **Comparison** tab in the Measurements table allows you to display lesions in the timepoint compared. - -![Comparison](../assets/img/LesionTracker/LT_Comparison.png) - -#### Key Timepoints - -**Key Timepoints** tab in the Measurements table allows you to display the lesions by the timepoints at the same time. - -![Key Timepoints](../assets/img/LesionTracker/LT_Key_Timepoints.png) - -#### Image Review - -To compare the lesions in Baseline and Follow-up, click the lesion record in the Measurements table. The recorded lesions will be displayed on the Baseline and Follow-up images - -#### Stack Scroll Tool** - -To scroll between two series synchronously, click More button and select **Stack Scroll Tool**. - -![Stack Scroll](../assets/img/LesionTracker/LT_StackScroll_Multiple.png) - -#### Case Progress - -Case Progress icon displays the progress of target and non-target lesions which are assessed out of total lesions. Number in the middle of the icon shows the number of lesions which are not assessed. - -![Assessment Progress](../assets/img/LesionTracker/LT_Assessment_Progress.png) - -#### Heads Up Display (HUD) Panel - -To have a quick view of the Measurements table, click **HUD** button at the top-right of the workspace. You can easily move and resize the HUD Panel on the workspace. - -![HUD Panel](../assets/img/LesionTracker/LT_HUD_Panel.png) - -Note: HUD button is only available if the study is associated with a timepoint. - -### Additional Tools -#### Temporary Length Measurement -To check the size of a lesion without adding to the Measurements table, select **Temp Tool**. -![Temp Tool](../assets/img/LesionTracker/LT_Temp_Tool.png) - -#### Stack Scroll Tool -To scroll images: - -1. Click More button and select **Stack Scroll Tool**. -2. Drag down and up on the image viewer with the left mouse button. - -![Stack Scroll](../assets/img/LesionTracker/LT_StackScroll_Single.png) - -#### Reset Image Tool -To reset the geometric orientation, position and magnification of the image in the active image viewer, click More button and select **Reset Tool**. -![Reset](../assets/img/LesionTracker/LT_Reset.png) - -#### Rotate Right Tool -To rotate the image in the active image viewer 90 degree clockwise, click More button and select **Rotate Right Tool**. -![Rotate Right](../assets/img/LesionTracker/LT_Rotate_Right.png) - -#### Flip H Tool -To flip the image in the active image viewer horizontally, click More button and select **Flip H Tool**. -![FlipH](../assets/img/LesionTracker/LT_FlipH.png) - -#### Flip V Tool -To flip the image in the active image viewer vertically, click More button and select **Flip V Tool**. -![FlipV](../assets/img/LesionTracker/LT_FlipV.png) - -#### Invert Tool -To invert the image in the active image viewer, click More button and select **Invert Tool**. -![Invert](../assets/img/LesionTracker/LT_Invert.png) - -#### Magnify Tool -Magnify Tool allows to zoom in a part of the image without changing zoom level of the whole image. - -1. Click More button and select **Magnify Tool**. -2. Click and drag around the image with the left mouse button. - -![Magnify](../assets/img/LesionTracker/LT_Magnify.png) - -#### 3.7.9. Ellipse Tool -Ellipse Tool allows you to draw elliptical annotation with mean and standard deviation values. - -1. Click More button and select **Ellipse Tool** then click on the image. -2. Hold, move and release the mouse button to create the annotation. - -![Ellipse](../assets/img/LesionTracker/LT_Ellipse.png) - -#### Save Screenshot -To save a screenshot of the current active viewport: - -1. Click More button and click **Save Screenshot**. -2. Set image name and sizes and click Download. - -![Download](../assets/img/LesionTracker/LT_Download.png) - -#### CINE Tool -CINE Tool allows to play through stacks of images in the active viewport. - -1. Click More button and click **CINE Tool**. The CINE dialog will pop up. -2. Click the desired media options from the CINE dialog. -3. To close CINE dialog, click **CINE Tool**. - -![CINE](../assets/img/LesionTracker/LT_CINE.png) diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/packages/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/packages/_category_.json deleted file mode 100644 index 55e5c9e73..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/packages/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Packages", - "position": 6 -} diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/packages/measurements.md b/platform/docs/versioned_docs/version-1.0-deprecated/packages/measurements.md deleted file mode 100644 index 4ac6f667f..000000000 --- a/platform/docs/versioned_docs/version-1.0-deprecated/packages/measurements.md +++ /dev/null @@ -1,15 +0,0 @@ -# Measurements Package (ohif-measurements) - -## Package design - -## Usage - -## How to define a Measurement tool - -## How to validate Measurements - -## Data exchange concepts - -## Longitudinal Measurements - -## Timepoints diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/Architecture.md b/platform/docs/versioned_docs/version-2.0-deprecated/Architecture.md deleted file mode 100644 index d61e3ffa1..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/Architecture.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -sidebar_position: 5 -title: Architecture ---- - -# Architecture - -Looking to extend your instance of the OHIF Viewer? Want learn how to reuse _a -portion_ of the Viewer in your own application? Or maybe you want to get -involved and draft or suggest a new feature? Regardless, you're in the right -place! - -The OHIF Viewer aims to be decoupled, configurable, and extensible; while this -allows our code to be used in more ways, it also increases complexity. Below, we -aim to demistify that complexity by providing insight into how our Viewer is -architected, and the role each of it's dependent libraries plays. - - - -The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a -[`monorepo`][monorepo]. This means that this repository, instead of containing a -single project, contains many projects. If you explore our project structure, -you'll see the following: - -```bash -. -├── extensions -│ ├── _example # Skeleton of example extension -│ ├── cornerstone # 2D images w/ Cornerstone.js -│ ├── dicom-html # Structured Reports as HTML in viewport -│ ├── dicom-microscopy # Whole slide microscopy viewing -│ ├── dicom-pdf # View DICOM wrapped PDFs in viewport -│ └── vtk # MPR and Volume support w/ VTK.js -│ -├── platform -│ ├── core # Business Logic -│ ├── i18n # Internationalization Support -│ ├── ui # React component library -│ └── viewer # Connects platform and extension projects -│ -├── ... # misc. shared configuration -├── lerna.json # MonoRepo (Lerna) settings -├── package.json # Shared devDependencies and commands -└── README.md -``` - -The `platform` directory contains the business logic library, component library, -and the application library that combines them to create a powerful medical -imaging viewer. - -The `extensions` directory contains many packages that can be registered with -`@ohif/core`'s `ExtensionManager` to expand an application's supported features -and functionality. - -![Architecture Diagram](./assets/img/architecture-diagram.png) - - -This diagram is a conceptual illustration of how the Viewer is architected. - -1. (optional) `extensions` can be registered with `@ohif/core`'s - `ExtensionManager` -2. `@ohif/core` provides business logic and a way for `@ohif/viewer` to access - registered extensions -3. The `@ohif/viewer` composes and provides data to components from our - component library (`@ohif/ui`) -4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an - embeddable package ([`@ohif/viewer`][viewer-npm]) - -## Business Logic - -The [`@ohif/core`][core-github] project offers pre-packaged solutions for -features common to Web-based medical imaging viewers. For example: - -- Hotkeys -- DICOM Web requests -- Hanging Protocols -- Managing a study's measurements -- Managing a study's DICOM metadata -- [A flexible pattern for extensions](./extensions/index.md) -- And many others - -It does this while remaining decoupled from any particular view library or -rendering logic. While we use it to power our React Viewer, it can be used with -Vue, React, Vanilla JS, or any number of other frameworks. - -## React Component Library - -[`@ohif/ui`][ui-github] is a React Component library that contains the reusable -components that power the OHIF Viewer. It allows us to build, compose, and test -components in isolation; easing the development process by reducing the need to -stand-up a local PACS with test case data. - -Extension authors can also use these same components when building their -extension's UI; allowing for a consistent look and feel with the rest of the -application. - -[Check out our component library!](https://react.ohif.org/) - -## Extensions & Configuration - -While OHIF maintains several high value and commonly requested features in its -own extensions, there are many instances where one may wish to further extend -the viewer. Some common use cases include: - -- Adding AI/ML tools and insights -- Custom workflows for guided diagnosis -- Collecting specific annotations for training data or reports -- Authentication and granular permissions -- Teleconsultation workflow, image comments, and tracking -- Adding surgical templating tools and reports -- and many others - -We expose common integration points via [extensions](./extensions/index.md) to -make this possible. The viewer and many of our own extensions also offer -[configuration][configuration]. For a list of extensions maintained by OHIF, -[check out this helpful table](./extensions/index.md#maintained-extensions). - -If you find yourself thinking "I wish the Viewer could do X", and you can't -accomplish it with an extension today, create a GitHub issue! We're actively -looking for ways to improve our extensibility ^\_^ - -[Click here to read more about extensions!](./extensions/index.md) - -## Common Questions - -> When should I use the packaged source `@ohif/viewer` versus building a PWA -> from the source? - -... - -> Can I create my own Viewer using Vue.js or Angular.js? - -You can, but you will not be able to leverage as much of the existing code and -components. `@ohif/core` could still be used for business logic, and to provide -a model for extensions. `@ohif/ui` would then become a guide for the components -you would need to recreate. - - - - -[monorepo]: https://github.com/OHIF/Viewers/issues/768 -[viewers-project]: https://github.com/OHIF/Viewers -[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer -[pwa]: https://developers.google.com/web/progressive-web-apps/ -[configuration]: ./configuring/index.md -[extensions]: ./extensions/index.md -[core-github]: https://github.com/OHIF/viewers/platform/core -[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui - diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/README.md b/platform/docs/versioned_docs/version-2.0-deprecated/README.md deleted file mode 100644 index 14e4553fa..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/README.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -id: Introduction -slug: / -sidebar_position: 1 ---- - - - -
- Looking for a Live Demo? -
- - - - - - - -> ATTENTION! You are looking at the docs for the `React` version of the OHIF -> Viewer. If you're looking for the `Meteor` version's documentation (now -> deprecated), select it's version from the dropdown box in the top right corner -> of this page. - -The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source, -web-based, medical imaging viewer. It can be configured to connect to Image -Archives that support [DicomWeb][dicom-web], and offers support for mapping to -proprietary API formats. OHIF maintained extensions add support for viewing, -annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes). - -![OHIF Viewer Screenshot](./assets/img/viewer.png) - -
The OHIF Viewer: A general purpose DICOM Viewer (Live Demo)
- -The Open Health Imaging Foundation intends to provide a simple general purpose -DICOM Viewer which can be easily extended for specific uses. If you find -yourself unable to extend the viewer for your purposes, please reach out via our -[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve -our integration and extension points. - -## Where to next? - -Check out these helpful links: - -- Ready to dive into some code? Check out our - [Getting Started Guide](./development/getting-started.md). -- We're an active, vibrant community. - [Learn how you can be more involved.](./development/contributing.md) -- Feeling lost? Read our [help page](./help.md). - - - - -[ohif-org]: https://www.ohif.org -[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb -[gh-issues]: https://github.com/OHIF/Viewers/issues - diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/architecture-diagram.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/architecture-diagram.png deleted file mode 100644 index 1c43d0108..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/architecture-diagram.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/dialog-example.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/dialog-example.gif deleted file mode 100644 index b6f9754c4..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/dialog-example.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-diagram.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-diagram.png deleted file mode 100644 index c71be8692..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-diagram.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-panel.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-panel.gif deleted file mode 100644 index fd173bd70..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-panel.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar-nested.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar-nested.gif deleted file mode 100644 index d89a75ab3..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar-nested.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar.gif deleted file mode 100644 index 88c313f3d..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-viewport.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-viewport.png deleted file mode 100644 index 0ecffda06..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-viewport.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/homePage.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/homePage.png deleted file mode 100644 index 9ae0624cc..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/homePage.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/lesionTracker.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/lesionTracker.png deleted file mode 100644 index effc7da90..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/lesionTracker.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/modal-example.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/modal-example.gif deleted file mode 100644 index 834705de1..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/modal-example.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/netlify-drop.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/netlify-drop.gif deleted file mode 100644 index 98634e088..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/netlify-drop.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/notification-example.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/notification-example.gif deleted file mode 100644 index 34d564cb4..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/notification-example.gif and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/viewer.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/viewer.png deleted file mode 100644 index 21eacd59a..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/viewer.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/worklist.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/worklist.png deleted file mode 100644 index 3f126c784..000000000 Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/worklist.png and /dev/null differ diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/_category_.json b/platform/docs/versioned_docs/version-2.0-deprecated/configuring/_category_.json deleted file mode 100644 index eef358871..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Configuring", - "position": 4 -} diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/data-source.md b/platform/docs/versioned_docs/version-2.0-deprecated/configuring/data-source.md deleted file mode 100644 index 6de5fa157..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/data-source.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -sidebar_position: 2 ---- -# Data Source - -After following the steps outlined in -[Getting Started](./../development/getting-started.md), you'll notice that the -OHIF Viewer has data for several studies and their images. You didn't add this -data, so where is it coming from? - -By default, the viewer is configured to connect to a remote server hosted by the -nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started, -the time may come when you want to develop using your own data either locally or -remotely. - -## Set up a local DICOM server - -> ATTENTION! Already have a remote or local server? Skip to the -> [configuration section](#configuration-learn-more) below. - -While the OHIF Viewer can work with any data source, the easiest to configure -are the ones that follow the [DICOMWeb][dicom-web] spec. - -1. Choose and install an Image Archive -2. Upload data to your archive (e.g. with DCMTK's [storescu][storescu] or your - archive's web interface) -3. Keep the server running - -For our purposes, we will be using `Orthanc`, but you can see a list of -[other Open Source options](#open-source-dicom-image-archives) below. - -### Requirements - -- Docker - - [Docker for Mac](https://docs.docker.com/docker-for-mac/) - - [Docker for Windows (recommended)](https://docs.docker.com/docker-for-windows/) - - [Docker Toolbox for Windows](https://docs.docker.com/toolbox/toolbox_install_windows/) - -_Not sure if you have `docker` installed already? Try running `docker --version` -in command prompt or terminal_ - -> If you are using `Docker Toolbox` you need to change the _PROXY_DOMAIN_ -> parameter in _platform/viewer/package.json_ to http://192.168.99.100:8042 or -> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy] -> uses to proxy requests - -### Running Orthanc - -_Start Orthanc:_ - -```bash -# Runs orthanc so long as window remains open -yarn run orthanc:up -``` - -_Upload your first Study:_ - -1. Navigate to - [Orthanc's web interface](http://localhost:8042/ui/app/index.html#/) at - `http://localhost:8042/ui/app/index.html#/` in a web browser. -2. In the left you can see the upload button where you can drag and drop your DICOM files - -#### Orthanc: Learn More - -You can see the `docker-compose.yml` file this command runs at -[`/.docker/Nginx-Orthanc/`][orthanc-docker-compose], and more on -Orthanc for Docker in [Orthanc's documentation][orthanc-docker]. - -### Connecting to Orthanc - -Now that we have a local Orthanc instance up and running, we need to configure -our web application to connect to it. Open a new terminal window, navigate to -this repository's root directory, and run: - -```bash -# If you haven't already, enable yarn workspaces -yarn config set workspaces-experimental true - -# Restore dependencies -yarn install - -# Run our dev command, but with the local orthanc config -yarn run dev:orthanc -``` - -#### Configuration: Learn More - -> For more configuration fun, check out the -> [Essentials Configuration](./index.md) guide. - -Let's take a look at what's going on under the hood here. `yarn run dev:orthanc` -is running the `dev:orthanc` script in our project's `package.json`. That script -is: - -```js -cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js -w -``` - -- `cross-env` sets three environment variables - - PROXY_TARGET: `/dicom-web` - - PROXY_DOMAIN: `http://localhost:8042` - - APP_CONFIG: `config/docker-nginx-orthanc.js` -- `webpack-dev-server` runs using the `.webpack/webpack.pwa.js` configuration - file. It will watch for changes and update as we develop. - -`PROXY_TARGET` and `PROXY_DOMAIN` tell our development server to proxy requests -to `Orthanc`. This allows us to bypass CORS issues that normally occur when -requesting resources that live at a different domain. - -The `APP_CONFIG` value tells our app which file to load on to `window.config`. -Here is what that -configuration looks like: - -```js title="/platform/viewer/public/config/default.js" -window.config = { - routerBasename: '/', - servers: { - dicomWeb: [ - { - name: 'Orthanc', - wadoUriRoot: 'http://localhost:8899/wado', - qidoRoot: 'http://localhost:8899/dicom-web', - wadoRoot: 'http://localhost:8899/dicom-web', - qidoSupportsIncludeField: false, - imageRendering: 'wadors', - thumbnailRendering: 'wadors', - }, - ], - }, -}; -``` - -To learn more about how you can configure the OHIF Viewer, check out our -[Configuration Guide](./index.md). - -## Open Source DICOM Image Archives - -Our example uses `Orthanc`, but there are a lot of options available to you. -Here are some of the more popular ones: - -| Archive | Installation | -| --------------------------------------------- | ---------------------------------- | -| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] | -| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] | -| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] | -| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client | -| [Horos][horos] (**Mac OSX only**) | Desktop Client | - -_Feel free to make a Pull Request if you want to add to this list._ - - - - -[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado -[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb -[storescu]: http://support.dcmtk.org/docs/storescu.html -[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy -[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/.docker/Nginx-Orthanc - -[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light -[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker -[orthanc]: https://www.orthanc-server.com/ -[orthanc-docker]: http://book.orthanc-server.com/users/docker.html -[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud -[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code -[osirix]: http://www.osirix-viewer.com/ -[horos]: https://www.horosproject.org/ - diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/index.md b/platform/docs/versioned_docs/version-2.0-deprecated/configuring/index.md deleted file mode 100644 index 3d602fa40..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/index.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Configuration - -> This step assumes you have an imaging archive. If you need assistance setting -> one up, check out the [`Data Source` Guide](./data-source.md) or a deployment -> recipe that contains an open Image Archive - - - -### Configuration Files - -The configuration for our viewer is in the `platform/viewer/public/config` -directory. Our build process knows which configuration file to use based on the -`APP_CONFIG` environment variable. By default, its value is -[`config/default.js`][default-config]. The majority of the viewer's features, -and registered extension's features, are configured using this file. - -**Embedded Use Note:** - -Alternatively, when using the `umd` bundle for embedded use cases, these same -values are what you'll pass to `installViewer` method: - -`OHIFStandaloneViewer.installViewer(window.config)` - -### Environment Variables - -We use environment variables at build and dev time to change the Viewer's -behavior. We can update the `HTML_TEMPLATE` to easily change which extensions -are registered, and specify a different `APP_CONFIG` to connect to an -alternative data source (or even specify different default hotkeys). - -| Environment Variable | Description | Default | -| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- | -| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` | -| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` | -| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` | -| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` | -| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` | - -## How do I configure my project? - -The simplest way is to update the existing default config: - - -```js title="/platform/viewer/public/config/default.js" -window.config = { - routerBasename: '/', - servers: { - dicomWeb: [ - { - name: 'DCM4CHEE', - wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', - qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', - wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', - qidoSupportsIncludeField: true, - imageRendering: 'wadors', - thumbnailRendering: 'wadors', - }, - ], - }, -}; -``` - -The configuration can also be written as a JS Function in case you need to inject dependencies like external services: - -```js -window.config = ({ servicesManager } = {}) => { - const { UIDialogService } = servicesManager.services; - return { - cornerstoneExtensionConfig: { - tools: { - ArrowAnnotate: { - configuration: { - getTextCallback: (callback, eventDetails) => UIDialogService.create({... - } - } - }, - }, - routerBasename: '/', - servers: { - dicomWeb: [ - { - name: 'DCM4CHEE', - wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', - qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', - wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', - qidoSupportsIncludeField: true, - imageRendering: 'wadors', - thumbnailRendering: 'wadors', - }, - ], - }, - }; -}; -``` - -You can also create a new config file and specify its path relative to the build -output's root by setting the `APP_CONFIG` environment variable. You can set the -value of this environment variable a few different ways: - -- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~ - - Previous `react-scripts` functionality that we need to duplicate with - `dotenv-webpack` -- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~ - - Previous `react-scripts` functionality that we need to duplicate with - `dotenv-webpack` -- Using the `cross-env` package in an npm script: - - `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"` - -After updating the configuration, `yarn run build` to generate updated build -output. - - - - -[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js -[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates -[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config - diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/index.md b/platform/docs/versioned_docs/version-2.0-deprecated/deployment/index.md deleted file mode 100644 index 5444026d7..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/index.md +++ /dev/null @@ -1,309 +0,0 @@ ---- -sidebar_position: 1 -sidebar_label: Overview ---- -# Deployment - -The OHIF Viewer can be embedded in other web applications via it's [packaged -script source][viewer-npm], or served up as a stand-alone PWA ([progressive web -application][pwa-url]) by building and hosting a collection of static assets. In -either case, you will need to configure your instance of the Viewer so that it -can connect to your data source (the database or PACS that provides the data -your Viewer will display). - - -Our goal is to make deployment as simple and painless as possible; however, -there is an inherent amount of complexity in configuring and deploying web -applications. If you find yourself a little lost, please don't hesitate to -[reach out for help](/help.md) - -## Deployment Scenarios - -### Embedded Viewer - -The quickest and easiest way to get the OHIF Viewer up and running is to embed -it into an existing web application. It allows us to forego a "build step", and -add a powerful medical imaging viewer to an existing web page using only a few -include tags. - -- Read more about it here: [Embedded Viewer](./recipes/embedded-viewer.md) -- And check out our [live demo on CodeSandbox][code-sandbox] - -![embeddedViewer](../assets/img/embedded-viewer-diagram.png) - - -### Stand-alone Viewer - -Deploying the OHIF Viewer as a stand-alone web application provides many -benefits, but comes at the cost of time and complexity. Some benefits include: - -_Today:_ - -- Leverage [extensions](/extensions/index.md) to drop-in powerful new features -- Add routes and customize the viewer's workflow -- Finer control over styling and whitelabeling - -_In the future:_ - -- The ability to package the viewer for [App Store distribution][app-store] -- Leverage `service-workers` for offline support and speed benefits from caching - -#### Hosted Static Assets - -At the end of the day, a production OHIF Viewer instance is a collection of -HTML, CSS, JS, Font Files, and Images. We "build" those files from our -`source code` with configuration specific to our project. We then make those -files publicly accessible by hosting them on a Web Server. - -If you have not deployed a web application before, this may be a good time to -[reach out for help](/help.md), as these steps assume prior web development and -deployment experience. - -##### Part 1 - Build Production Assets - -"Building", or creating, the files you will need is the same regardless of the -web host you choose. You can find detailed instructions on how to configure and -build the OHIF Viewer in our -["Build for Production" guide](./recipes/build-for-production.md). - -##### Part 2 - Host Your App - -There are a lot of [benefits to hosting static assets][host-static-assets] over -dynamic content. You can find instructions on how to host your build's output -via one of these guides: - -_Drag-n-drop_ - -- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop) - -_Easy_ - -- [Surge.sh](/deployment/recipes/static-assets.md#surgesh) -- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages) - -_Advanced_ - -- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront) -- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare) -- [Azure](/deployment/recipes/static-assets.md#azure) - -## Data - -The OHIF Viewer is able to connect to any data source that implements the [DICOM -Web Standard][dicom-web-standard]. [DICOM Web][dicom-web] refers to RESTful -DICOM Services -- a recently standardized set of guidelines for exchanging -medical images and imaging metadata over the internet. Not all archives fully -support it yet, but it is gaining wider adoption. - -### Configure Connection - -If you have an existing archive and intend to host the OHIF Viewer at the same -domain name as your archive, then connecting the two is as simple as following -the steps laid out in our -[Configuration Essentials Guide](./../configuring/index.md). - -#### What if I don't have an imaging archive? - -We provide some guidance on configuring a local image archive in our -[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an -archive remotely is a little trickier. You can check out some of our -[advanced recipes](#recipes) for modeled setups that may work for you. - -#### What if I intend to host the OHIF Viewer at a different domain? - -There are two important steps to making sure this setup works: - -1. Your Image Archive needs to be exposed, in some way, to the open web. This - can be directly, or through a `reverse proxy`, but the Viewer needs _some - way_ to request it's data. -2. \* Your Image Archive needs to have appropriate CORS (Cross-Origin Resource - Sharing) Headers - -> \* Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional -> HTTP headers to tell a browser to let a web application running at one origin -> (domain) have permission to access selected resources from a server at a -> different origin. - [MDN Web Docs: Web - Http - CORS][cors] - -Most image archives do not provide either of these features "out of the box". -It's common to use IIS, Nginx, or Apache to route incoming requests and append -appropriate headers. You can find an example of this setup in our -[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md). - -#### What if my archive doesn't support DicomWeb? - -It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint. -You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.) - -This JSON is supplied via the '?url=' query parameter. -It should reference an endpoint that returns **application/json** formatted text. - -If you do not have an API, you can simply return a text file containing the JSON from any web server. - - -You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route: - -eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json` - -The returned JSON object must contain a single root object with a 'studies' array. - - -*Sample JSON format:* -```json -{ - "studies": [ - { - "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78", - "StudyDescription": "BRAIN SELLA", - "StudyDate": "20010108", - "StudyTime": "120022", - "PatientName": "MISTER^MR", - "PatientId": "832040", - "series": [ - { - "SeriesDescription": "SAG T-1", - "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", - "SeriesNumber": 2, - "SeriesDate": "20010108", - "SeriesTime": "120318", - "Modality": "MR", - "instances": [ - { - "metadata": { - "Columns": 512, - "Rows": 512, - "InstanceNumber": 3, - "AcquisitionNumber": 0, - "PhotometricInterpretation": "MONOCHROME2", - "BitsAllocated": 16, - "BitsStored": 16, - "PixelRepresentation": 1, - "SamplesPerPixel": 1, - "PixelSpacing": [0.390625, 0.390625], - "HighBit": 15, - "ImageOrientationPatient": [0,1,0,0,0,-1], - "ImagePositionPatient": [11.600000,-92.500000, 98.099998], - "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", - "ImageType": ["ORIGINAL","PRIMARY","OTHER"], - "Modality": "MR", - "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124", - "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", - "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" - }, - "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm" - } - ] - } - ] - } - ] -} -``` -More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500) - - -**Implementation Notes:** - -1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route. -2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly. - eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm` -3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet. -4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500). - -*CORS Issues (Cross-Origin Resource Sharing)* - -If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers. -Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors. -You will to need add a configuration on each server hosting these assets to allow your App server origin. - -For example: - -Lets assume your application is hosted on `https://my-ohif-server.com`. - -Your JSON API is hosted on `https://my-json-api.aws.com` - -And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com` - -When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`. - -Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this. - -Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`. -You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`. - -Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing. - - -### Securing Your Data - -> Feeling lost? Securing your data is important, and it can be hard to tell if -> you've gotten it right. Don't hesitate to work with professional auditors, or -> [enlist help from experts](./../help.md). - -The OHIF Viewer can be configured to work with authorization servers that -support one or more of the OpenID-Connect authorization flows. The Viewer finds -it's OpenID-Connect settings on the `oidc` configuration key. You can set these -values following the instructions laid out in the -[Configuration Essentials Guide](./../configuring/index.md). - -_Example OpenID-Connect Settings:_ - -```js -window.config = { - ... - oidc: [ - { - // ~ REQUIRED - // Authorization Server URL - authority: 'http://127.0.0.1/auth/realms/ohif', - client_id: 'ohif-viewer', - redirect_uri: 'http://127.0.0.1/callback', // `OHIFStandaloneViewer.js` - response_type: 'code', // "Authorization Code Flow" - scope: 'openid', // email profile openid - // ~ OPTIONAL - post_logout_redirect_uri: '/logout-redirect.html', - }, - ], -} -``` - -You can find an example of this setup in our -[User Account Control Deployment Recipe](./recipes/user-account-control.md). - -#### Choosing a Flow for the Viewer - -In general, we recommend using the "Authorization Code Flow" ( [see -`response_type=code` here][code-flows]); however, the "Implicit Flow" ( [see -`response_type=token` here][code-flows]) can work if additional precautions are -taken. If the flow you've chosen produces a JWT Token, it's validity can be used -to secure access to your Image Archive as well. - -### Recipes - -We've included a few recipes for common deployment scenarios. There are many, -many possible configurations, so please don't feel limited to these setups. -Please feel free to suggest or contribute your own recipes. - -- Script Include - - [Embedding the Viewer](./recipes/embedded-viewer.md) -- Stand-Alone - - [Build for Production](./recipes/build-for-production.md) - - [Static](./recipes/static-assets.md) - - [Nginx + Image Archive](./recipes/nginx--image-archive.md) - - [User Account Control](./recipes/user-account-control.md) - - - - -[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer -[pwa-url]: https://developers.google.com/web/progressive-web-apps/ -[static-assets-url]: https://www.maxcdn.com/one/visual-glossary/static-content/ -[app-store]: https://medium.freecodecamp.org/i-built-a-pwa-and-published-it-in-3-app-stores-heres-what-i-learned-7cb3f56daf9b -[dicom-web-standard]: https://www.dicomstandard.org/dicomweb/ -[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb -[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/ -[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS -[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660 -[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch - diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/_category_.json b/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/_category_.json deleted file mode 100644 index 5b36cc15d..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Recipes", - "position": 2 -} diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/embedded-viewer.md b/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/embedded-viewer.md deleted file mode 100644 index b3a125659..000000000 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/embedded-viewer.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -sidebar_position: 1 ---- -# Embedded Viewer - -The quickest and easiest way to get the OHIF Viewer up and running is to embed -it into an existing web application. It allows us to forego a "build step", and -add a powerful medical imaging viewer to an existing web page using only a few -include tags. Here's how it works: - - - -![embeddedViewer](../../assets/img/embedded-viewer-diagram.png) - - - - -1. Create a new web page or template that includes the following external - dependencies: - - - -
    -
  1. Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some - example values that would allow the viewer to hit our public PACS:
  2. -
- -```js -// Set before importing `ohif-viewer` (JS Object) -window.config = { - // default: '/' - routerBasename: '/', - servers: { - dicomWeb: [ - { - name: 'DCM4CHEE', - wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', - qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', - wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', - qidoSupportsIncludeField: true, - imageRendering: 'wadors', - thumbnailRendering: 'wadors', - }, - ], - }, -}; -``` - -To learn more about how you can configure the OHIF Viewer, check out our -[Configuration Guide](../../configuring/index.md). - -
  1. - Render the viewer in the web page's target div -
- -```js -// Made available by the `@ohif/viewer` script included in step 1 -var containerId = 'id-of-div-to-render-component-to'; -var componentRenderedOrUpdatedCallback = function() { - console.log('OHIF Viewer rendered/updated'); -}; -window.OHIFViewer.installViewer( - window.config, - containerId, - componentRenderedOrUpdatedCallback -); -``` - -You can see a live example of this recipe in [this CodeSandbox][code-sandbox]. - -## Add Extensions - -The UMD build of the OHIF Viewer is a "light weight" build that only contains -the core extensions required for basic 2D image viewing. It's possible to add -other extensions at runtime. - -This only requires us to include a single script tag, and add it using the -`extensions` key to our config. In this practical example, we register our -popular whole slide microscopy extension: - -```html - - - - -``` - -You can see an example of a slide microscopy study in the viewer [with the -extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox]) -and [without it here][whole-slide-base-demo] ([source code][code-sandbox]). - -You can read more about extensions and how to create your own in our -[extensions guide](/extensions/index.md). - -#### FAQ - -> I'm having trouble getting this to work. Where can I go for help? - -First, check out this fully functional [CodeSandbox][code-sandbox] example. If -you're still having trouble, feel free to search or GitHub issues. Can't find -anything related your problem? Create a new one. - -> My application's styles are impacting the OHIF Viewer's look and feel. What -> can I do? - -When you include stylesheets and scripts, they are added globally. This has the -potential of causing conflicts with other scripts and styles on the page. To -prevent this, `embed` the viewer in a new/empty web page. Have that working? -Good. Now `embed` that new page using an -[` +
+ +[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light +[dcm4chee-docker]: + https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker +[orthanc]: https://www.orthanc-server.com/ +[orthanc-docker]: http://book.orthanc-server.com/users/docker.html +[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud +[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code +[osirix]: http://www.osirix-viewer.com/ +[horos]: https://www.horosproject.org/ +[default-config]: + https://github.com/OHIF/Viewers/blob/master/platform/app/public/config/default.js +[html-templates]: + https://github.com/OHIF/Viewers/tree/master/platform/app/public/html-templates +[config-files]: + https://github.com/OHIF/Viewers/tree/master/platform/app/public/config +[storescu]: http://support.dcmtk.org/docs/storescu.html +[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy diff --git a/platform/docs/versioned_docs/version-3.8/configuration/dataSources/introduction.md b/platform/docs/versioned_docs/version-3.8/configuration/dataSources/introduction.md new file mode 100644 index 000000000..2a0add515 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/configuration/dataSources/introduction.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Data Source + +The internal data structure of OHIF’s metadata follows naturalized DICOM JSON, a +format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords +instead of tags and sequences as arrays, for easy development and clear code. + +Here in this section we will discuss couple of data sources that are commonly used +and OHIF has provided the implementation for them. + +## Custom Data Source +Do you have a custom data source? or a custom data that you want to use in OHIF? +You can easily write a data source to map your data to OHIF’s native format. + +You can read more in the [Data Source Module](../../platform/extensions/modules/data-source.md) diff --git a/platform/docs/versioned_docs/version-3.8/configuration/dataSources/static-files.md b/platform/docs/versioned_docs/version-3.8/configuration/dataSources/static-files.md new file mode 100644 index 000000000..2741e0495 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/configuration/dataSources/static-files.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 5 +sidebar_label: Static Files +--- + +# Static Files + +There is a binary DICOM to static file generator, which provides easily served +binary files. The files are all compressed in order to reduce space +significantly, and are pre-computed for the files required for OHIF, so that the +performance of serving the files is just the read from disk/write to http stream +time, without any extra processing time. + +The project for the static wado files is located here: [static-wado]: +https://github.com/OHIF/static-wado + +It can be compiled with Java and Gradle, and then run against a set of dicom, in +the example located in /dicom/study1 outputting to /dicomweb, and then a server +run against that data, like this: + +```bash +git clone https://github.com/OHIF/static-wado +cd static-wado +./gradlew installDist +StaticWado/build/install/StaticWado/bin/StaticWado -d /dicomweb /dicom/study1 +cd /dicomweb +npx http-server -p 5000 --cors -g + +# you can use npx serve ./dist -l 8080 -c ../public/serve.json as an alternative to http-server +``` + +There is then a dev environment in the platform/app directory which can be +run against those files, like this: + +``` +cd platform/app +yarn dev:static +``` + +Additional studies can be added to the dicomweb by re-running the StaticWado +command. It will create a single studies.gz index file (JSON DICOM file, +compressed) containing an index of all studies created. There is then a small +extension to OHIF which performs client side indexing. + +The StaticWado command also knows how to deploy a client and dicomweb directory +to Amazon s3, which can then server files up directly. There is another build +setup build:aws in the viewer package.json to create such a deployment. diff --git a/platform/docs/versioned_docs/version-3.8/configuration/url.md b/platform/docs/versioned_docs/version-3.8/configuration/url.md new file mode 100644 index 000000000..cac7e5ad4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/configuration/url.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 3 +sidebar_label: URL +--- + +# URL + +You can modify the URL at any state of the app to get the desired result. Here +are different part of the APP that you can modify: + + +## WorkList + +The WorkList can be modified by adding the following query parameters: + +### PatientName + +The patient name can be modified by adding the `PatientName` query parameter. + +```js +/?patientName=myQuery +``` + +### MRN + +The MRN can be modified by adding the `MRN` query parameter. + +```js +/?mrn=myQuery +``` + +### Description + +The description can be modified by adding the `Description` query parameter. + +```js +/?description=myQuery +``` + +### Modality + +The modality can be modified by adding the `modalities` query parameter. + +```js +/?modalities=MG +``` + +### Accession Number + +The accession number can be modified by adding the `accession` query parameter. + +```js +/?accession=myQuery +``` + +### DataSources + +If you happen to have multiple data sources configured, you can filter the +WorkList by adding the `dataSources` query parameter. + +```js +/?dataSources=orthanc +``` + +Note1: You should pass the `sourceName` of the data source in the configuration file (not the friendly name nor the name) +Note2: Make sure that the configuration file you are using actually includes that data source. You cannot use a data source from another configuration file. + + +:::tip + +You can add `sortBy` and `sortDirection` query parameters to sort the WorkList + +```js +/?patientName=myquery&sortBy=studyDate&sortDirection=ascending +``` + +::: + + +## Viewer + +The Viewer can be modified by adding the following query parameters: + + +### Mode + +As you have seen before, the Viewer can be configured to be in different modes. +Each mode registers their `id` in the URL. + +For instance + +```js +/viewer?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 +``` + +will open the viewer in the basic (longitudinal) mode with the StudyInstanceUID +1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339. + +And if configured, the same study can be opened in the `tmtv` mode + +```js +/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 +``` + +### StudyInstanceUIDs + +You can open more than one study in the Viewer by adding the `StudyInstanceUIDs` + + +```js +/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1 +``` + +:::tip + +You can use this feature to open a current and prior study in the Viewer. +Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section. You can also use commas to separate +values. + +::: + + +### SeriesInstanceUIDs + +Sometimes you need to only retrieve a specific series in a study, you can do +that by providing series level QIDO query parameters in the URL such as +SeriesInstanceUIDs. This does NOT work with instance or study +level parameters. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8 +``` + +This will only open the viewer with one series (one displaySet) loaded, and no +queries made for any other series. + +Sometimes you need to only retrieve a subset of series in a study, you can do +that by providing more than one series, separated by commas. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10 +``` + +This will only open the viewer with two series (two displaySets) loaded, and no +queries made for any other series. + +### initialSeriesInstanceUID + +Alternatively, sometimes you want to just open the study on a specified series, but allowing other +series to be present too. This is the same behavior can be +achieved by using the `initialSeriesInstanceUID` parameter. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8 +``` + +This will open all the series in the study, but the viewer will start with the +series specified by the `initialSeriesInstanceUID` parameter. + + +Note that you can combine these, if you want to load a specific set of series +plus show an initial one as the first one selected, for example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10 +``` + +### initialSopInstanceUID + +You can also specify the initial SOP Instance to be displayed by using the +`initialSopInstanceUID` parameter. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095501.9 +``` + +This will open the study with the filtered series, and navigate to the slice 101 +which happens to be the SOP Instance specified by the `initialSopInstanceUID` + +Note: again you can mix and match + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8 +``` + +You can even load the whole study and only specify the initial SOP Instance to be displayed. Although +it will take more time to match, but it works as expected. + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8 +``` + +### hangingProtocolId + +You can select the initial hanging protocol to apply by using the +hangingProtocolId parameter. The selected parameter must be available in a +hangingProtocolModule registration, but does not have to be active. + +For instance for loading a specific study in mpr mode from start you can use: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingProtocolId=@ohif/mnGrid +``` + +### token + +Although not recommended, you can use the token param in the URL which will inject +the token into the Authorization header of the request. diff --git a/platform/docs/versioned_docs/version-3.8/conformance.md b/platform/docs/versioned_docs/version-3.8/conformance.md new file mode 100644 index 000000000..62c9c40f8 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/conformance.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 10 +sidebar_label: DICOM Conformance Statement (NEW) +title: DICOM Conformance Statement +--- + +You can find a version that has been open sourced by Radical Imaging [in this link](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/_category_.json b/platform/docs/versioned_docs/version-3.8/deployment/_category_.json similarity index 64% rename from platform/docs/versioned_docs/version-2.0-deprecated/deployment/_category_.json rename to platform/docs/versioned_docs/version-3.8/deployment/_category_.json index 88be8d879..534be1dfb 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/_category_.json +++ b/platform/docs/versioned_docs/version-3.8/deployment/_category_.json @@ -1,4 +1,4 @@ { "label": "Deployment", - "position": 8 + "position": 3 } diff --git a/platform/docs/versioned_docs/version-3.8/deployment/authorization.md b/platform/docs/versioned_docs/version-3.8/deployment/authorization.md new file mode 100644 index 000000000..b80a7c962 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/deployment/authorization.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 6 +sidebar_label: Authorization +--- + +# Authorization +The OHIF Viewer can be configured to work with authorization servers that support one or more of the OpenID-Connect authorization flows. The Viewer finds it's OpenID-Connect settings on the oidc configuration key. You can set these values in your configuration files. For instance you can take a look at our +`google.js` configuration file. + + +```js +oidc: [ + { + // ~ REQUIRED + // Authorization Server URL + authority: 'https://accounts.google.com', + client_id: + '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', + response_type: 'id_token token', + scope: + 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid + // ~ OPTIONAL + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + }, + ], +``` + +You need to provide the following information: +- authority: The URL of the authorization server. +- client_id: The client id of your application (provided by the authorization server). +- redirect_uri: The callback URL of your application. +- response_type: The response type of the authorization flow (e.g. id_token token, [learn more about different flows](https://darutk.medium.com/diagrams-of-all-the-openid-connect-flows-6968e3990660)). +- scope: The scopes that your application needs to access +- post_logout_redirect_uri: The URL that the user will be redirected to after logout. +- revoke_uri: The URL that the user will be redirected to after logout. +- automaticSilentRenew: If true, the user will be automatically logged in after the token expires. +- revokeAccessTokenOnSignout: If true, the access token will be revoked on logout. + + + +## How it works +The Viewer uses the `userAuthenticationService` to set the OpenID-Connect settings. The `userAuthenticationService` is a singleton service that is responsible for authentication and authorization. It is initialized by the app and you can grab it +from the `servicesManager` + +```js +const userAuthenticationService = servicesManager.services.userAuthenticationService; +``` + +Then the userAuthenticationService will inject the token as Authorization header in the requests that are sent to the server (both metadata +and pixelData). + + +## Token based authentication +Sometimes (although not recommended), some servers like to send the token +in the query string. In this case, the viewer will automatically grab the token from the query string +and add it to the userAuthenticationService and remove it from the query string (to prevent it from being logged in the console +in future requests). + +and example would be + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.2.3.4.5.6.6.7&token=e123125jsdfahsdf +``` diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/build-for-production.md b/platform/docs/versioned_docs/version-3.8/deployment/build-for-production.md similarity index 64% rename from platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/build-for-production.md rename to platform/docs/versioned_docs/version-3.8/deployment/build-for-production.md index 1c633b79f..d1dd508bd 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/build-for-production.md +++ b/platform/docs/versioned_docs/version-3.8/deployment/build-for-production.md @@ -1,14 +1,9 @@ --- sidebar_position: 2 --- + # Build for Production -> If you've already followed the -> ["Getting Started" Guide](/development/getting-started.md), you can skip ahead -> to [Configuration](#configuration) - -## Overview - ### Build Machine Requirements - [Node.js & NPM](https://nodejs.org/en/download/) @@ -29,7 +24,7 @@ More on: _[`git clone`](https://git-scm.com/docs/git-clone), _From .zip:_ -[OHIF/Viewers: react.zip](https://github.com/OHIF/Viewers/archive/master.zip) +[OHIF/Viewers: master.zip](https://github.com/OHIF/Viewers/archive/master.zip) ### Restore Dependencies & Build @@ -37,21 +32,20 @@ Open your terminal, and navigate to the directory containing the source files. Next run these commands: ```bash -// If you haven't already, enable yarn workspaces +# If you haven't already, enable yarn workspaces yarn config set workspaces-experimental true -// Restore dependencies +# Restore dependencies yarn install -// Build source code for production +# Build source code for production yarn run build ``` If everything worked as expected, you should have a new `dist/` directory in the -project's folder. It should roughly resemble the following: +`platform/app/dist` folder. It should roughly resemble the following: -```bash -platform/viewer/dist/ +```bash title="platform/app/dist/" ├── app-config.js ├── app.bundle.js ├── app.css @@ -68,7 +62,7 @@ how to configure the project for your own imaging archive below. ### Configuration -The configuration for our viewer is in the `platform/viewer/public/config` +The configuration for our viewer is in the `platform/app/public/config` directory. Our build process knows which configuration file to use based on the `APP_CONFIG` environment variable. By default, its value is [`config/default.js`][default-config]. The majority of the viewer's features, @@ -76,7 +70,7 @@ and registered extension's features, are configured using this file. The easiest way to apply your own configuration is to modify the `default.js` file. For more advanced configuration options, check out our -[configuration essentials guide](/configuring/index.md). +[configuration essentials guide](../configuration/configurationFiles.md). ## Next Steps @@ -84,18 +78,18 @@ file. For more advanced configuration options, check out our _Drag-n-drop_ -- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop) +- [Netlify: Drop](./static-assets#netlify-drop) _Easy_ -- [Surge.sh](/deployment/recipes/static-assets.md#surgesh) -- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages) +- [Surge.sh](./static-assets#surgesh) +- [GitHub Pages](./static-assets#github-pages) _Advanced_ -- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront) -- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare) -- [Azure](/deployment/recipes/static-assets.md#azure) +- [AWS S3 + Cloudfront](./static-assets#aws-s3--cloudfront) +- [GCP + Cloudflare](./static-assets#gcp--cloudflare) +- [Azure](./static-assets#azure) ### Testing Build Output Locally @@ -104,17 +98,27 @@ You can do this by running the following commands in the `dist/` output directory: ```bash -// Install http-server as a globally available package +# Install http-server as a globally available package yarn global add http-server -// Serve the files in our current directory -// Accessible at: `http://localhost:8080` -http-server +# Change the directory to the platform/app + +# Serve the files in our current directory +npx serve ./dist -c ../public/serve.json ``` +:::caution +In the video below notice that there is `platform/viewer` which has been renamed to `platform/app` in the latest version +::: + +
+ +
+ + ### Automating Builds and Deployments -If you found setting up your environmnent and running all of these steps to be a +If you found setting up your environment and running all of these steps to be a bit tedious, then you are in good company. Thankfully, there are a large number of tools available to assist with automating tasks like building and deploying web application. For a starting point, check out this repository's own use of: @@ -123,16 +127,10 @@ web application. For a starting point, check out this repository's own use of: - [Netlify][netlify]: [netlify.toml][netlify.toml] | [build-deploy-preview.sh][build-deploy-preview.sh] -## Troubleshooting - -> Issues and resolutions for common GitHub issues will be summarized here - -... - [circleci]: https://circleci.com/gh/OHIF/Viewers [circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml [netlify]: https://app.netlify.com/sites/ohif/deploys -[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/netlify.toml +[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/platform/app/netlify.toml [build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh diff --git a/platform/docs/versioned_docs/version-3.8/deployment/cors.md b/platform/docs/versioned_docs/version-3.8/deployment/cors.md new file mode 100644 index 000000000..135c9f961 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/deployment/cors.md @@ -0,0 +1,313 @@ +--- +sidebar_position: 7 +--- + +# Cross-Origin Information for OHIF + +This document describes various security configurations, settings and environments/contexts needed to fully leverage OHIF’s capabilities. One may need some configurations while others might need ALL of them - it all depends on the environment OHIF is expected to run in. + +In particular, three of OHIF’s features depend on these configurations: +- [OHIF’s use of SharedArrayBuffer](#sharedarraybuffer) +- [Embedding OHIF in an iframe](#embedding-ohif-in-an-iframe) +- [XMLHttpRequests to fetch data from data sources](#cors-in-ohif) + + + +## SharedArrayBuffer +A `SharedArrayBuffer` is a JavaScript object that is similar to an `ArrayBuffer` but can be shared between web workers and the window that spawned them via the `postMessage` API. See [SharedArrayBuffer in MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) for more information. + +:::tip +To turn off Shared Array Buffer completely, just set `useSharedArrayBuffer` to `false` in the [OHIF configuration](../configuration/configurationFiles.md). But keep in mind that you will not get the performance boost that Shared Array Buffer offers for decoding and rendering big volumes where web workers write to the same memory space. +::: + +### Security Requirements + +In order to use `SharedArrayBuffer` objects in the browser, the following security conditions must be met: + +- The page must be served in a [secure context](#secure-context). +- The page must have [cross-origin isolation](#cross-origin-isolation) enabled. + +### `SharedArrayBuffer` in OHIF + +OHIF uses `SharedArrayBuffer` in its volume loader (from Cornerstone3D). It comes with the benefit of improved performance and optimization at the cost of some configuration to use it. + +As such, if the following popup is shown when launching OHIF then the OHIF server will have to be configured to permit the loading of volumetric images and data. Note that stack viewports are still available and functional even when this error is present. + +![OHIF in non-secure context](../assets/img/ohif-non-secure-context.png) + +To better determine which (if not all) of the [security requirements](#security-requirements) are lacking, have a look at the browser console. + +Output in the console similar to the following indicates that OHIF is not running in a [secure context](#secure-context). + +![browser console for non-secure context](../assets/img/browser-console-non-secure-context.png) + +Absence of the above error in the console together with the presence of the Cross Origin Isolation popup warning, likely indicates that either or both of the [COOP](#coop---cross-origin-opener-policy) and/or [COEP](#coep---cross-origin-embedder-policy) headers are not set for OHIF. + +## Embedding OHIF in an iframe + +As described [here](./iframe.md), there are cases where OHIF will be embedded in an iframe. The following links provide more information for setting up and configuring OHIF to work in an iframe: + +- [OHIF iframe documentation](./iframe.md#static-build) +- [OHIF as a Cross-origin Resource in an iframe](#ohif-as-a-cross-origin-resource-in-an-iframe) + +## Secure Context + +MDN defines a secure context as [“a Window or Worker for which certain minimum standards of authentication and confidentiality are met.“](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) + +Any local URL is considered secure. The following are some examples of local URLs that are considered secure… +- http://localhost +- http://127.0.0.1:3000 + +URLs that are NOT local must be delivered over `https://` or `wss://` (i.e. TLS) to be considered secure. See [When is a context considered secure](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure) in MDN for more information. + +### iframes + +A page embedded in an iframe is considered secure if it itself and every one of its embedding ancestors are delivered securely. Otherwise it is deemed insecure. + +### Why does OHIF require a secure context? + +Beyond all of the inherent benefits of a secure connection, OHIF requires a secure context so that it can utilize [SharedArrayBuffer](#sharedarraybuffer) objects for volume rendering. + +### Configuring/setting up a secure context + +[Local URLs are considered secure](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure), and as such whenever OHIF is accessed via a local URL (e.g. http://localhost:3000) it is running in a secure context. For example, in a development environment using the default webpack setup, OHIF can be deployed and accessed in a secure context at http://localhost:3000. + +The best alternative is to host OHIF over HTTPS. + +:::tip +OHIF can be served over HTTPS in a variety of ways (these are just some examples). +- Website hosting services that offer HTTPS deployment (e.g,. Netlify) or offer HTTPS load balancers (AWS, Google Cloud etc.) +- Setting up a reverse proxy (e.g. `nginx`) with a self-signed certificate that forwards requests to the OHIF server + - [An OHIF Docker image can be set up this way](./docker.md#ssl). +::: + +## Origin Definition + +According to [MDN](https://developer.mozilla.org/en-US/docs/Glossary/Origin), a Web content’s origin is defined by the scheme (protocol), hostname (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, hostname, and port all match. + +## CORS - Cross-Origin Resource Sharing + +A cross-origin resource is a resource (e.g. image, JSON, etc) that is served by one origin and used/referenced by a different origin. + +CORS is the protocol utilized by web servers and browsers whereby a server of one origin identifies and/or restricts which of its resources that other origins (i.e. other than its own) a browser should allow access to. By default a browser does not permit cross-origin resource sharing. + +The CORS mechanism relies on the HTTP response headers from the server to indicate if a resource can be shared with a different origin. + +See the [MDN CORS article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for more information. + +### CORS HTTP Headers + +The header that mostly concerns OHIF is listed below and should be configured accordingly on the DICOMweb server or any data source that OHIF would make XMLHttpRequests to for its data. + +```http +Access-Control-Allow-Origin: `` | * +``` + +:::tip +The `Access-Control-Allow-Origin` header specifies which origins can access the served resource embedded in the response. + +Either a single, specific origin (i.e. ``) can be specified or ALL origins (i.e. *) + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin) for more information. +::: + +### CORS in OHIF + +OHIF fetches and displays data and images from data sources. It invokes XMLHttpRequests to some data sources such as DICOMweb data sources to fetch the information to render. Typically, a DICOMweb server is hosted on a completely different origin than the one serving OHIF. As such, those XMLHttpRequests use CORS. + +### Troubleshooting CORS in OHIF + +The following is an example screenshot of the browser console when one of OHIF’s DICOMweb data source servers is not configured for CORS. + +![CORS browser console errors](../assets/img/cors-browser-console-errors.png) + +And the following is what is in the accompanying network tab. + +![CORS browser network panel errors](../assets/img/cors-network-panel-errors.png) + +:::info +Setting the appropriate CORS header varies per server or service that is hosting the data source. What follows below is just one example to remedy the problem. +::: + +:::tip +If Orthanc is the data source running in a Docker container composed with/behind nginx. And OHIF is being served at localhost:3000. The issue can be remedied by adding either of the following to Orthanc’s Docker container nginx.conf file. + +```nginx +add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; +``` + +Or + +```nginx +add_header 'Access-Control-Allow-Origin' '*' always; +``` +::: + +## COOP - Cross-Origin Opener Policy + +The COOP HTTP response header restricts the global, root document of the page from being referenced and accessed by another cross-origin document that might open the page in a window. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) for more information. + +### Header Values Pertinent to OHIF (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy#syntax) for more information) + +|Value|Description| +|-----|-----------| +|same-origin|Restricts the document to be referenced by openers of the same origin only.| + +### COOP in OHIF + +COOP is required for [SharedArrayBuffer](#sharedarraybuffer) usage in OHIF. See also [Troubleshooting Cross-origin Isolation in OHIF](#troubleshooting-cross-origin-isolation-in-ohif). + +## COEP - Cross-Origin Embedder Policy + +The COEP HTTP response header restricts cross-origin documents from being embedded into a document (e.g. in an iframe, video, image, etc). See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) for more information. + +### Header Values Pertinent to OHIF (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy#syntax) for more information) + +|Value|Description| +|-----|-----------| +|require-corp|Permits the document to load either of the following embedded resources:
  • Those from the same origin
  • Cross-origin resources embedded by a DOM element that has the appropriate [crossorigin attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) set
  • Cross-origin resources with the appropriate [CORP response header](#corp---cross-origin-resource-policy)
+|credentialless|See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy#syntax) for more information| + +### COEP in OHIF + +COEP is required for [SharedArrayBuffer](#sharedarraybuffer) usage in OHIF. See also [Troubleshooting Cross-origin Isolation in OHIF](#troubleshooting-cross-origin-isolation-in-ohif). + +## Cross-origin Isolation + +Cross-origin isolation is [enabled](https://web.dev/cross-origin-isolation-guide/#enable-cross-origin-isolation) for a web page when the following COOP and COEP headers are set. +- [COOP](#coop---cross-origin-opener-policy) with `same-origin` +- [COEP](#coep---cross-origin-embedder-policy) with `require-corp` or `credentialless` + +### iframe + +An iframe is considered to have cross-origin isolation enabled if it itself has the appropriate COOP and COEP headers set as well as every one of its embedding ancestors. + +### Troubleshooting Cross-origin Isolation in OHIF + +The [SharedArrayBuffer in OHIF](#sharedarraybuffer-in-ohif) section describes how to determine if there are problems with cross-origin isolation in OHIF. If it is determined that COOP and/or COEP is indeed an issue, then the COOP and COEP headers must be set for OHIF. How to accomplish this varies per server or service that is hosting OHIF. The following are just a few examples. + +:::tip +In the default dev environment, the following can be set in the webpack.pwa.js file… + +```javascript +devServer: { + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" + } +} +``` +::: + +:::tip +If deploying OHIF using Netlify, the Netlify configuration [file](https://docs.netlify.com/configure-builds/file-based-configuration/) can be used to configure the headers as such… + +``` +[[headers]] + # Define which paths this specific [[headers]] block will cover. + for = "/*" + + [headers.values] + Cross-Origin-Opener-Policy = "same-origin" + Cross-Origin-Embedder-Policy = "require-corp" +``` +::: + +:::tip +If OHIF is served behind nginx, then the headers can be set in the nginx.conf file as follows. The [template nginx configuration file](https://github.com/OHIF/Viewers/blob/master/.docker/Viewer-v3.x/default.conf.template) for creating a [OHIF Docker image](./docker.md#building-the-docker-image) has an example of this too. +```nginx +server { + location / { + add_header Cross-Origin-Opener-Policy same-origin; + add_header Cross-Origin-Embedder-Policy require-corp; + } +} +``` +::: + +## CORP - Cross-Origin Resource Policy + +The CORP HTTP response header indicates which origins can read and use a resource. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy) for more information. + +### Header Values (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy#usage) for more information) + +|Value|Description| +|-----|-----------| +|same-site|Only requests from the same site can read the resource.| +|same-origin|Only requests from the same origin can read the resource.| +|cross-origin|Requests from any origin can read the resource. The value is useful and [exists](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy#relationship_to_cross-origin_embedder_policy_coep) primarily for letting documents with the [COEP require-corp value](#header-values-pertinent-to-ohif-see-mdn-for-more-information-1) know that the resource is ok to be embedded| + +### OHIF and CORP + +There are two scenarios where the CORP header is relevant to OHIF: + +- [PDF from a Cross Origin DICOMweb Data Source](#pdf-from-a-cross-origin-dicomweb-data-source) +- [OHIF as a Cross-origin Resource in an iframe](#ohif-as-a-cross-origin-resource-in-an-iframe) + +Both these scenarios stem from the fact that OHIF has to be served with the [COEP](#coep---cross-origin-embedder-policy) header to support [SharedArrayBuffer](#sharedarraybuffer). + +#### PDF from a Cross Origin DICOMweb Data Source + +There are some DICOMweb data sources (e.g. dcm4chee) whereby OHIF uses the data source’s `/rendered` endpoint to embed a DICOM PDF document in the OHIF DOM using an `` tag. + +As specified for the [COEP require-corp value](#header-values-pertinent-to-ohif-see-mdn-for-more-information-1), a page like OHIF with COEP header `require-corp` can embed cross-origin resources in DOM elements that have the [`crossorigin` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) OR the resource is delivered with an appropriate CORP header. The `` tag does NOT support the `crossorigin` attribute. As such, the PDF must be delivered with a CORP header. + +:::tip +Setting the CORP header varies per server or service that is hosting the data source. The following is just one example. + +For a dcm4chee DICOMweb data source composed in Docker behind nginx, the CORP header can be configured in the nginx.conf file as such: + +```nginx +add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always; +``` + +If the dcm4chee server and the OHIF server are hosted on the same site, then the following would also work: +```nginx +add_header 'Cross-Origin-Resource-Policy' 'same-site' always; +``` +::: + +#### OHIF as a Cross-origin Resource in an iframe + +There are cases where [OHIF is embedded in an iframe](./iframe.md) and the embedding page is from a different origin. Again due to the [security requirements for SharedArrayBuffer](#security-requirements), [both OHIF and the embedding page](#iframe) must have the appropriate COEP header. In this scenario, OHIF is the cross-origin resource and since the ` + + + ### Troubleshooting @@ -114,6 +132,14 @@ Stop running all containers: - Win: `docker ps -a -q | ForEach { docker stop $_ }` - Linux: `docker stop $(docker ps -a -q)` + +_Traceback (most recent call last):_ + _File "urllib3/connectionpool.py", line 670, in urlopen_ + _...._ + +Are you sure your docker is running? see explanation [here](https://github.com/docker/compose/issues/7896) + + ### Configuration After verifying that everything runs with default configuration values, you will @@ -172,7 +198,7 @@ Here are some resources, specific to this setup, that may be helpful: - [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl) - [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/) -While we terminate SSL at Nginx, it may be worth using self signed certificates +While we terminate SSL at Nginx, it may be worth using self-signed certificates for communication between services. - [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/) @@ -240,7 +266,7 @@ members put together: [orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration [lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc -[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile -[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf -[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json +[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/dockerfile +[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/config/nginx.conf +[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/config/orthanc.json diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/static-assets.md b/platform/docs/versioned_docs/version-3.8/deployment/static-assets.md similarity index 68% rename from platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/static-assets.md rename to platform/docs/versioned_docs/version-3.8/deployment/static-assets.md index ebacc0ea2..4186a1b3c 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/static-assets.md +++ b/platform/docs/versioned_docs/version-3.8/deployment/static-assets.md @@ -1,7 +1,7 @@ --- sidebar_position: 3 -sidebar_label: Assets --- + # Deploy Static Assets > WARNING! All of these solutions stand-up a publicly accessible web viewer. Do @@ -31,7 +31,7 @@ _Advanced_ - [Deploy Static Assets](#deploy-static-assets) - [Drag-n-drop](#drag-n-drop) - - [Netlify: Drop](#netlify-drop) + - [Netlify Drop](#netlify-drop) - [Easy](#easy) - [Surge.sh](#surgesh) - [GitHub Pages](#github-pages) @@ -42,13 +42,13 @@ _Advanced_ ## Drag-n-drop -### Netlify: Drop +### Netlify Drop -> Build, deploy, and manage modern web projects. An all-in-one workflow that -> combines global deployment, continuous integration, and automatic HTTPS. And -> that’s just the beginning. -![netlify-drop example](../../assets/img/netlify-drop.gif) +
+ +
+ _GIF demonstrating deployment with Netlify Drop_ @@ -77,7 +77,7 @@ Learn more about [Netlify on their website](https://www.netlify.com/) > publishing. Publish HTML, CSS, and JS for free, without leaving the command > line. -![surge.sh deploy example](../../assets/img/surge-deploy.gif) +![surge.sh deploy example](../assets/img/surge-deploy.gif) _GIF demonstrating deployment with surge_ @@ -111,7 +111,7 @@ Learn more about [surge.sh on their website](https://surge.sh/) > Websites for you and your projects. Hosted directly from your GitHub > repository. Just edit, push, and your changes are live. -This deploy strategy makes more sense if you intend to maintain your project in +This deployment strategy makes more sense if you intend to maintain your project in a GitHub repository. It allows you to specify a `branch` or `folder` as the target for a GitHub Page's website. As you push code changes, the hosted content updates to reflect those changes. @@ -140,7 +140,7 @@ avoid using a proxy or to simplify authentication. If that is the case, check out some of our more advanced `docker` deployments that target these providers from the left-hand sidepanel. -These guides can be a bit longer and a update more frequently. To provide +These guides can be a bit longer and an update more frequently. To provide accurate documentation, we will link to each provider's own recommended steps: ### AWS S3 + Cloudfront @@ -155,6 +155,22 @@ accurate documentation, we will link to each provider's own recommended steps: ### Azure -- [Host a Static Website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website) + - Deploying viewer to Azure blob storage as a static website: + Refer to [Host a static website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website) + High level steps : + 1. Go to Azure portal and create a storage account. + 2. Under Overview->Capabilities, select Static website. + 3. Enable Static website. Set the index document as ‘index.html’. + 4. Copy the primary endpoint. This will serve as the root URL for the viewer. + 5. Save. A new container named ‘$web’ will be created. + 6. Copy OHIF viewer’s build output from ‘platform\app\dist’ folder to the ‘$web’ container. + 7. Open browser and navigate to the viewer root URL copied in the step above. It should display OHIF viewer with data from default data source. + + ![image](https://github.com/OHIF/Viewers/assets/132684122/236a574b-0f05-4d90-a721-df8720d05949) + Special consideration while accessing DicomJson data source : + • Due to the way routing is handled in react, it may error out in production when trying to display data through dicomJson data source. E.g. https://[Static Website endpoint]/viewer/dicomjson?url= https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json + • Resolution to this is to set error page to ‘index.html’ at the website level. This will ensure that all errors are redirected to root and requests are further served from root path. + ![image](https://github.com/OHIF/Viewers/assets/132684122/87696c90-c344-489a-af15-b992434555f9) + - [Add SSL Support](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-https-custom-domain-cdn) - [Configure a Custom Domain](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name) diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/user-account-control.md b/platform/docs/versioned_docs/version-3.8/deployment/user-account-control.md similarity index 97% rename from platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/user-account-control.md rename to platform/docs/versioned_docs/version-3.8/deployment/user-account-control.md index 65bab3d9b..5447ce282 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/user-account-control.md +++ b/platform/docs/versioned_docs/version-3.8/deployment/user-account-control.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 11 --- # User Account Control @@ -35,7 +35,7 @@ applications and services with little to no code. We improve upon our This setup allows us to create a setup similar to the one pictured below: -![userControlFlow](../../assets/img/user-access-control-request-flow.png) +![userControlFlow](../assets/img/user-access-control-request-flow.png) @@ -63,7 +63,7 @@ in command prompt or terminal_ _Spin Things Up_ -- Navigate to `/docker/OpenResty-Orthanc-Keycloak` in your shell +- Navigate to `platform\app\.recipes\OpenResty-Orthanc-Keycloak` in your shell - Run `docker-compose up` _Create Your First User_ @@ -168,7 +168,7 @@ to leverage custom themes in | Default Theme | OHIF Theme | | ---------------------------------------------------------------------- | ---------------------------------------------------------------- | -| ![Keycloak Default Theme](../../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../../assets/img/keycloak-ohif-theme.png) | +| ![Keycloak Default Theme](../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../assets/img/keycloak-ohif-theme.png) | ## Next Steps diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/development/_category_.json b/platform/docs/versioned_docs/version-3.8/development/_category_.json similarity index 65% rename from platform/docs/versioned_docs/version-2.0-deprecated/development/_category_.json rename to platform/docs/versioned_docs/version-3.8/development/_category_.json index 3f6ccc7e8..8627cac49 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/development/_category_.json +++ b/platform/docs/versioned_docs/version-3.8/development/_category_.json @@ -1,4 +1,4 @@ { "label": "Development", - "position": 3 + "position": 5 } diff --git a/platform/docs/versioned_docs/version-3.8/development/architecture.md b/platform/docs/versioned_docs/version-3.8/development/architecture.md new file mode 100644 index 000000000..f8755c68a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/development/architecture.md @@ -0,0 +1,205 @@ +--- +sidebar_position: 2 +sidebar_label: Architecture +--- + +# Architecture + +In order to achieve a platform that can support various workflows and be +extensible for the foreseeable future we went through extensive planning of +possible use cases and decided to significantly change and improve the +architecture. + +Below, we aim to demystify that complexity by providing insight into how +`OHIF Platform` is architected, and the role each of its dependent libraries +plays. + +## Overview + +The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a +[`monorepo`][monorepo]. This means that this repository, instead of containing a +single project, contains many projects. If you explore our project structure, +you'll see the following: + +```bash +│ +├── extensions +│ ├── default # default functionalities +│ ├── cornerstone # 2D/3D images w/ Cornerstonejs +│ ├── cornerstone-dicom-sr # Structured reports +│ ├── measurement-tracking # measurement tracking +│ └── dicom-pdf # View DICOM wrapped PDFs in viewport +| # and many more ... +│ +├── modes +│ └── longitudinal # longitudinal measurement tracking mode +| └── basic-dev-mode # basic viewer with Cornerstone (a developer focused mode) +| # and many more +│ +├── platform +│ ├── core # Business Logic +│ ├── i18n # Internationalization Support +│ ├── ui # React component library +│ └── app # Connects platform and extension projects +│ +├── ... # misc. shared configuration +├── lerna.json # MonoRepo (Lerna) settings +├── package.json # Shared devDependencies and commands +└── README.md +``` + +OHIF v3 is composed of the following components, described in detail in further +sections: + +- `@ohif/app`: The core framework that controls extension registration, mode + composition and routing. +- `@ohif/core`: A library of useful and reusable medical imaging functionality + for the web. +- `@ohif/ui`: A library of reusable components to build OHIF-styled applications + with. +- `Extensions`: A set of building blocks for building applications. The OHIF org + maintains a few core libraries. +- `Modes`: Configuration objects that tell @ohif/app how to compose + extensions to build applications on different routes of the platform. + +## Extensions + +The `extensions` directory contains many packages that provide essential +functionalities such as rendering, study/series browsers, measurement tracking +that modes can consume to enable a certain workflow. Extensions have had their +behavior changed in `OHIF-v3` and their api is expanded. In summary: + +> In `OHIF-v3`, extensions no longer automatically hook themselves to the app. +> Now, registering an extension makes its component available to `modes` that +> wish to use them. Basically, extensions in `OHIF-v3` are **building blocks** +> for building applications. + +OHIF team maintains several high value and commonly used functionalities in its +own extensions. For a list of extensions maintained by OHIF, +[check out this helpful table](../platform/extensions/index.md#maintained-extensions). +As an example `default` extension provides a default viewer layout, a +study/series browser and a datasource that maps to a DICOMWeb compliant backend. + +[Click here to read more about extensions!](../platform/extensions/index.md) + +## Modes + +The `modes` directory contains workflows that can be registered with OHIF within +certain `routes`. The mode will get used once the user opens the viewer on the +registered route. + +OHIF extensions were designed to provide certain core functionalities for +building your viewer. However, often in medical imaging we face a specific use +case in which we are using some core functionalities, adding our specific UI, +and use it in our workflows. Previously, to achieve this you had to create an +extension to add have such feature. `OHIF-v3` introduces `Modes` to enable +building such workflows by re-using the core functionalities from the +extensions. + +Some common workflows may include: + +- Measurement tracking for lesions +- Segmentation of brain abnormalities +- AI probe mode for detecting prostate cancer + +In the mentioned modes above, they will share the same core rendering module +that the `default` extension provides. However, segmentation mode will require +segmentation tools which is not needed for the other two. As you can see, modes +are a layer on top of extensions, that you can configure in order to achieve +certain workflows. + +To summarize the difference between extensions and modes in `OHIF-v3` and +extensions in `OHIF-v2` + +> - `Modes` are configuration objects that tell _@ohif/app_ how to compose +> extensions to build applications on different routes of the platform. +> - In v2 extensions are “plugins” that add functionality to a core viewer. +> - In v3 extensions are building blocks that a mode uses to build an entire +> viewer layout. + +[Click here to read more about modes!](../platform/modes/index.md) + +## Platform + +### `@ohif/app` + +This library is the core library which consumes modes and extensions and builds +an application. Extensions can be passed in as app configuration and will be +consumed and initialized at the appropriate time by the application. Upon +initialization the viewer will consume extensions and modes and build up the +route desired, these can then be accessed via the study list, or directly via +url parameters. + +Upon release modes will also be plugged into the app via configuration, but this +is still an area which is under development/discussion, and they are currently +pulled from the window in beta. + +Future ideas for this framework involve only adding modes and fetching the +required extension versions at either runtime or build time, but this decision +is still up for discussion. + +### `@ohif/core` + +OHIF core is a carefully maintained and tested set of web-based medical imaging +functions and classes. This library includes managers and services used from +within the viewer app. + +OHIF core is largely similar to the @ohif/core library in v2, however a lot of +logic has been moved to extensions: however all logic about DICOMWeb and other +data fetching mechanisms have been pulled out, as these now live in extensions, +described later. + +### `@ohif/ui` + +Firstly, a large time-consumer/barrier for entry we discovered was building new +UI in a timely manner that fit OHIF’s theme. For this reason we have built a new +UI component library which contains all the components one needs to build their +own viewer. + +These components are presentational only, so you can reuse them with whatever +logic you desire. As the components are presentational, you may swap out +@ohif/ui for a custom UI library with conforming API if you wish to white label +the viewer. The UI library is here to make development easier and quicker, but +it is not mandatory for extension components to use. + +[Check out our component library!](https://ui.ohif.org/) + +## Overview of the architecture + +OHIF-v3 architecture can be seen in the following figure. We will explore each +piece in more detail. + +![mode-archs](../assets/img/mode-archs.png) + +## Common Questions + +> Can I create my own Viewer using Vue.js or Angular.js? + +You can, but you will not be able to leverage as much of the existing code and +components. `@ohif/core` could still be used for business logic, and to provide +a model for extensions. `@ohif/ui` would then become a guide for the components +you would need to recreate. + +> When I want to implement a functionality, should it be in the mode or in an +> extension? + +This is a great question. Modes are designed to consume extensions, so you +should implement your functionality in one of the modules of your new extension, +and let the mode consume it. This way, in the future, if you needed another mode +that utilizes the same functionality, you can easily hook the extension to the +new mode as well. + + + + +[monorepo]: https://github.com/OHIF/Viewers/issues/768 +[viewers-project]: https://github.com/OHIF/Viewers +[viewer-npm]: https://www.npmjs.com/package/@ohif/app +[pwa]: https://developers.google.com/web/progressive-web-apps/ +[configuration]: ../configuration/index.md +[extensions]: ../platform/extensions/index.md +[core-github]: https://github.com/OHIF/viewers/platform/core +[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui + diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/development/continuous-integration.md b/platform/docs/versioned_docs/version-3.8/development/continuous-integration.md similarity index 71% rename from platform/docs/versioned_docs/version-2.0-deprecated/development/continuous-integration.md rename to platform/docs/versioned_docs/version-3.8/development/continuous-integration.md index c07618671..48ad4c4d0 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/development/continuous-integration.md +++ b/platform/docs/versioned_docs/version-3.8/development/continuous-integration.md @@ -1,10 +1,11 @@ --- -sidebar_position: 3 -title: Continuous Integration +sidebar_position: 8 +sidebar_label: Continuous Integration --- + # Continuous Integration (CI) -This repository uses `CircleCI` and `Netlify` for continuous integration. +This repository uses `CircleCI` and `Netlify` for Continuous integration. ## Deploy Previews @@ -29,14 +30,6 @@ The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and end-to-end tests for every code check-in. These tests must all pass before code can be merged to our `master` branch. - - - ![PR_CHECKS](../assets/img/WORKFLOW_PR_CHECKS.png) ### Workflow: PR_OPTIONAL_DOCKER_PUBLISH @@ -45,16 +38,7 @@ The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish the pull request as a tagged docker image. This is helpful when changes need to be tested with the Google Adapter before merging to `master`. - - -![publish](../assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png) - - +![PR_Workflow](../assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png) > NOTE: This workflow will fail unless it's for a branch on our `upstream` > repository. If you need this functionality, but the branch is from a fork, @@ -67,14 +51,7 @@ It uses the Netlify CLI to deploy assets created as part of the repository's PWA Build process (`yarn run build`). The workflow allows for "Manual Approval" to promote the build to `STAGING` and `PRODUCTION` environments. - - -![deploy](../assets/img/WORKFLOW_DEPLOY.png) +![WORKFLOW_DEPLOY](../assets/img/WORKFLOW_DEPLOY.png) | Environment | Description | URL | | ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- | @@ -95,19 +72,7 @@ GitHub hosts the `gh-pages` branch with GitHub Pages. - Extension Packages: https://github.com/ohif/viewers/#extensions - Documentation: https://docs.ohif.org/ - - - -![release](../assets/img/WORKFLOW_RELEASE.png) - -### HOTFIX - -_Not yet implemented_ +![WORKFLOW_RELEASE](../assets/img/WORKFLOW_RELEASE.png) #### Other linkage notes @@ -97,6 +97,11 @@ there are a lot of factors that can influence the behavior of our development server and bundler. If you encounter issues not addressed here, please don't hesitate to reach out on GitHub. +Sometimes you might encounter a situation where the linking doesn't work as +expected. This might happen when there are multiple linked packages with the +same name. You can [remove][unlink] the linked packages inside yarn and try +again. + ## Any guidance on submitting changes? While we do appreciate code contributions, triaging and integrating contributed @@ -112,7 +117,7 @@ working on your pull requests: We will provide feedback on your pull requests as soon as possible. Following the tips above will help ensure your changes are reviewed. -## Testing contribution pull requests + diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/development/getting-started.md b/platform/docs/versioned_docs/version-3.8/development/getting-started.md similarity index 63% rename from platform/docs/versioned_docs/version-2.0-deprecated/development/getting-started.md rename to platform/docs/versioned_docs/version-3.8/development/getting-started.md index 219bd1f62..d170e83fa 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/development/getting-started.md +++ b/platform/docs/versioned_docs/version-3.8/development/getting-started.md @@ -1,7 +1,8 @@ --- sidebar_position: 1 -title: Getting Started +sidebar_label: Getting Started --- + # Getting Started ## Setup @@ -23,6 +24,7 @@ up-to-date with the upstream (original) repository. This is called a "Triangular Workflow" and is common for Open Source projects. The GitHub blog has a [good graphic that illustrates this setup][triangular-workflow]. + ### Private Alternatively, if you intend to use the OHIF Viewer as a starting point, and you @@ -38,6 +40,29 @@ aren't as concerned with syncing updates, then follow these steps: ## Developing + +### Branches + +#### `master` branch - The latest dev (beta) release + +- `master` - The latest dev release + +This is typically where the latest development happens. Code that is in the master branch has passed code reviews and automated tests, but it may not be deemed ready for production. This branch usually contains the most recent changes and features being worked on by the development team. It's often the starting point for creating feature branches (where new features are developed) and hotfix branches (for urgent fixes). + +Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1` + +### `release/*` branches - The latest stable releases +Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production. + +For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch. + +Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases. + +Here is a schematic representation of our development workflow: + + + + ### Requirements - [Node.js & NPM](https://nodejs.org/en/) @@ -61,25 +86,20 @@ yarn run dev You should see the following output: ```bash -@ohif/viewer: i 「wds」: Project is running at http://localhost:3000/ -@ohif/viewer: i 「wds」: webpack output is served from / -@ohif/viewer: i 「wds」: Content not from webpack is served from D:\code\ohif\Viewers\platform\viewer -@ohif/viewer: i 「wds」: 404s will fallback to /index.html +@ohif/app: i 「wds」: Project is running at http://localhost:3000/ +@ohif/app: i 「wds」: webpack output is served from / +@ohif/app: i 「wds」: Content not from webpack is served from D:\code\ohif\Viewers\platform\viewer +@ohif/app: i 「wds」: 404s will fallback to /index.html # And a list of all generated files ``` ### 🎉 Celebrate 🎉 - - -
- +
- - ### Building for Production > More comprehensive guides for building and publishing can be found in our @@ -94,7 +114,7 @@ yarn run build - If you receive a _"No Studies Found"_ message and do not see your studies, try changing the Study Date filters to a wider range. -- If you see a 'Loading' message which never resolves, check your browser +- If you see a 'Loading' message which never resolves, check your browser's JavaScript console inside the Developer Tools to identify any errors. diff --git a/platform/docs/versioned_docs/version-3.8/development/link.md b/platform/docs/versioned_docs/version-3.8/development/link.md new file mode 100644 index 000000000..c06fa24e9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/development/link.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 9 +sidebar_label: Local Linking +--- + +# Introduction + +Local linking allows you to develop and test a library in the context of an application before it's published or when you encounter +a bug that you suspect is related to a library. With Yarn, this can be achieved through the yarn link command. + +The general procedure is as follows: + + +Link the Library: + +```sh +cd /path/to/library +yarn link +``` + +This command will create a symlink in a global directory for the library. + + +Link to the Application: + +```sh +cd /path/to/application +yarn link "library-name" +``` + +Creates a symlink from the global directory to the application's node_modules. + + +# Tutorial for linking Cornerstone3D to OHIF + +Below we demonstrate how to link Cornerstone3D to OHIF Viewer. This is useful for testing and debugging Cornerstone3D in the context of OHIF Viewer. + +
+ +
+ +::tip +Since `@cornerstonejs/tools` depends on `@cornerstonejs/core`, if you need the changes +you made in `@cornerstonejs/core` to be reflected in `@cornerstonejs/tools`, you need to +also link `@cornerstonejs/core` to `@cornerstonejs/tools`. + +```sh +cd /path/to/cornerstonejs-core +# for the core +yarn link + +cd /path/to/cornerstonejs-tools +yarn link "@cornerstonejs/core" + +# for the tools +yarn link + +# inside OHIF +cd /path/to/OHIFViewer +yarn link "@cornerstonejs/core" +yarn link "@cornerstonejs/tools" +``` diff --git a/platform/docs/versioned_docs/version-3.8/development/ohif-cli.md b/platform/docs/versioned_docs/version-3.8/development/ohif-cli.md new file mode 100644 index 000000000..038f331a9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/development/ohif-cli.md @@ -0,0 +1,312 @@ +--- +sidebar_position: 3 +sidebar_label: OHIF CLI +--- + +# OHIF Command Line Interface + +OHIF-v3 architecture has been re-designed to enable building applications that +are easily extensible to various use cases (Modes) that behind the scene would +utilize desired functionalities (Extensions) to reach the goal of the use case. +Now, the question is _how to create/remove/install/uninstall an extension and/or +mode?_ + +You can use the `cli` script that comes with the OHIF monorepo to achieve these +goals. + +:::note Info +In the long-term, we envision our `cli` tool to be a separate installable +package that you can invoke anywhere on your local system to achieve the same +goals. In the meantime, `cli` will remain as part of the OHIF monorepo and needs +to be invoked using the `yarn` command. +::: + + +## CLI Installation + +You don't need to install the `cli` currently. You can use `yarn` to invoke its +commands. + +## Commands + +:::note Important +All commands should run from the root of the monorepo. +::: + + +There are various commands that can be used to interact with the OHIF-v3 CLI. If +you run the following command, you will see a list of available commands. + +``` +yarn run cli --help +``` + +which will output + +``` +OHIF CLI + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + create-extension Create a new template extension + create-mode Create a new template Mode + add-extension [version] Adds an ohif extension + remove-extension removes an ohif extension + add-mode [version] Removes an ohif mode + remove-mode Removes an ohif mode + link-extension Links a local OHIF extension to the Viewer to be used for development + unlink-extension Unlinks a local OHIF extension from the Viewer + link-mode Links a local OHIF mode to the Viewer to be used for development + unlink-mode Unlinks a local OHIF mode from the Viewer + list List Added Extensions and Modes + search [options] Search NPM for the list of Modes and Extensions + help [command] display help for command +``` + +As seen there are commands for you such as: `create-extension`, `create-mode`, +`add-extension`, `remove-extension`, `add-mode`, `remove-mode`, +`link-extension`, `unlink-extension`, `link-mode`, `unlink-mode`, `list`, +`search`, and `help`. Here we will go through each of the commands and describe +them. + +### create-mode + +If you need to create a new mode, you can use the `create-mode` command. This +command will create a new mode template in the directory that you specify. +The command will ask you couple of information/questions in order +to properly create the mode metadata in the `package.json` file. + +```bash +yarn run cli create-mode +``` + +
+ +![image](../assets/img/create-mode.png) + + +
+ +Note 1: Some questions have a default answer, which is indicated inside the +parenthesis. If you don't want to answer the question, just hit enter. It will +use the default answer. + +Note 2: As you see in the questions, you can initiate a git repository for the +new mode right away by answering `Y` (default) to the question. + +Note 3: Finally, as indicated by the green lines at the end, `create-mode` command only +create the mode template. You will need to link the mode to the Viewer in order +to use it. See the [`link-mode`](#link-mode) command. + +If we take a look at the directory that we created, we will see the following +files: + +
+ +![image](../assets/img/mode-template.png) + +
+ + +### create-extension + +Similar to the `create-mode` command, you can use the `create-extension` +command to create a new extension template. This command will create a new +extension template in the directory that you specify the path. + +```bash +yarn run cli create-extension +``` + + +Note: again similar to the `create-extension` command, you need to manually link +the extension to the Viewer in order to use it. See the +[`link-mode`](#link-mode) command. + + +### link-extension + +`link-extension` command will link a local OHIF extension to the Viewer. This +command will utilize `yarn link` to achieve so. + +```bash +yarn run cli link-extension +``` + +### unlink-extension + +There might be situations where you want to unlink an extension from the Viewer +after some developments. `unlink-extension` command will do so. + +```bash +ohif-cli unlink-extension +``` + + + +### link-mode + +Similar to the `link-extension` command, `link-mode` command will link a local +OHIF mode to the Viewer. + +```bash +yarn run cli link-mode +``` + +### unlink-mode + +Similar to the `unlink-extension` command, `unlink-mode` command will unlink a +local OHIF mode from the Viewer. + +```bash +ohif-cli unlink-mode +``` + +### add-mode + +OHIF is a modular viewer. This means that you can install (add) different modes +to the viewer if they are published online . `add-mode` command will add a new mode to +the viewer. It will look for the mode in the NPM registry and installs it. This +command will also add the extension dependencies that the mode relies on to the +Viewer (if specified in the peerDependencies section of the package.json). + +:::note Important +`cli` will validate the npm package before adding it to the Viewer. An OHIF mode +should have `ohif-mode` as one of its keywords. +::: + +Note: If you don't specify the version, the latest version will be used. + +```bash +yarn run cli add-mode [version] +``` + +For instance `@ohif-test/mode-clock` is an example OHIF mode that we have +published to NPM. This mode basically has a panel that shows the clock :) + +We can add this mode to the Viewer by running the following command: + +```bash +yarn run cli add-mode @ohif-test/mode-clock +``` + +After installation, the Viewer has a new mode! + + +![image](../assets/img/add-mode.png) + + +Note: If the mode has an extension peerDependency (in this case @ohif-test/extension-clock), +`cli` will automatically add the extension to the Viewer too. + +The result + +![image](../assets/img/clock-mode.png) +![image](../assets/img/clock-mode1.png) + +### add-extension + +This command will add an OHIF extension to the Viewer. It will look for the +extension in the NPM registry and install it. + +```bash +yarn run cli add-extension [version] +``` + + +### remove-mode + +This command will remove the mode from the Viewer and also remove the extension +dependencies that the mode relies on from the Viewer. + +```bash +yarn run cli remove-mode +``` + + +### remove-extension + +Similar to the `remove-mode` command, this command will remove the extension +from the Viewer. + +```bash +yarn run cli remove-extension +``` + +### list + +`list` command will list all the installed extensions and modes in +the Viewer. It uses the `PluginConfig.json` file to list the installed +extensions and modes. + +```bash +yarn run cli list +``` + +an output would look like this: + +
+ +![image](../assets/img/ohif-cli-list.png) + +
+ +### search + +Using `search` command, you can search for OHIF extensions and modes +in the NPM registry. This tool can accept a `--verbose` flag to show more +information about the results. + +```bash +yarn run cli search [--verbose] +``` + +
+ +![image](../assets/img/cli-search-no-verbose.png) + +
+ +with the verbose flag `ohif-cli search --verbose` you will achieve the following +output: + +
+ +![image](../assets/img/cli-search-with-verbose.png) + +
+ + +## PluginConfig.json + +To make all the above commands work, we have created a new file called `PluginConfig.json` which contains the +information needed to run the commands. You **don't need to (and should not)** +edit/update/modify this file as it is automatically generated by the CLI. You +can take a look at what this file contains by going to +`platform/app/PluginConfig.json` in your project's root directory. In short, +this file tracks and stores all the extensions/modes and the their version that +are currently being used by the viewer. + +## Private NPM Repos + +For the `yarn cli` to view private NPM repos, create a read-only token with the +following steps and export it as an environmental variable. You may also export +an existing npm token. +``` +npm login +npm token create --read-only +export NPM_TOKEN= +``` + +## External dependencies +The ohif-cli will add the path to the external dependencies to the webpack config, +so that you can install them in your project and use them in your custom +extensions and modes. To achieve this ohif-cli will update the webpack.pwa.js +file in the platform/app directory. + +## Video tutorials +See the [Video Tutorials](./video-tutorials.md) for videos of some the above +commands in action. diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/our-process.md b/platform/docs/versioned_docs/version-3.8/development/our-process.md similarity index 95% rename from platform/docs/versioned_docs/version-2.0-deprecated/our-process.md rename to platform/docs/versioned_docs/version-3.8/development/our-process.md index c6d7e51c3..a420c9d49 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/our-process.md +++ b/platform/docs/versioned_docs/version-3.8/development/our-process.md @@ -1,7 +1,8 @@ --- -sidebar_position: 2 -title: Our process +sidebar_position: 6 +sidebar_label: Issue & PR Triage Process --- + # Our Process Our process is a living, breathing thing. We strive to have regular @@ -32,15 +33,15 @@ first contact with would-be adopters and contributors, it's important that we strive for timely responses and satisfactory resolutions. We attempt to accomplish this by: -1. Responding to issues requiring `triage` at least once a week +1. Responding to issue requiring `triage` at least once a week 2. Create new "official issues" from "community issues" 3. Provide clear guidance and next steps (when applicable) 4. Regularly clean up old (stale) issues -> :pencil: Less obviously, patterns in the issues being reported can highlight -> areas that need improvement. For example, users often have difficulty -> navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce -> our ticket volume for this issue? +> 🖋 Less obviously, patterns in the issues being reported can highlight areas +> that need improvement. For example, users often have difficulty navigating +> CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket +> volume for this issue? ### Backlogged Issues @@ -96,7 +97,7 @@ appropriate: We rely on GitHub Checks and integrations with third party services to evaluate changes in code quality and test coverage. Tests must pass and User cases must be present (when applicable) before a PR can be merged to master, and code -quality and test coverage must not changed by a significant margin. For some +quality and test coverage must not be changed by a significant margin. For some repositories, visual screenshot-based tests are also included, and video recordings of end-to-end tests are stored for later review. diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/development/testing.md b/platform/docs/versioned_docs/version-3.8/development/testing.md similarity index 68% rename from platform/docs/versioned_docs/version-2.0-deprecated/development/testing.md rename to platform/docs/versioned_docs/version-3.8/development/testing.md index c8c66bc4b..23f239755 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/development/testing.md +++ b/platform/docs/versioned_docs/version-3.8/development/testing.md @@ -1,11 +1,93 @@ --- -sidebar_position: 4 -title: Testing +sidebar_position: 7 +sidebar_label: Testing --- -# Tests + +# Running Tests for OHIF + +We introduce here various test types that is available for OHIF, and how to run +each test in order to make sure your contribution hasn't broken any existing +functionalities. Idea and philosophy of each testing category is discussed in +the second part of this page. + +## Unit test + +To run the unit test: + +```bash +yarn run test:unit:ci +``` + +Note: You should have already installed all the packages with `yarn install`. + +Running unit test will generate a report at the end showing the successful and +unsuccessful tests with detailed explanations. + +## End-to-end test +For running the OHIF e2e test you need to run the following steps: + +- Open a new terminal, and from the root of the OHIF mono repo, run the following command: + + ```bash + yarn test:data + ``` + + This will download the required data to run the e2e tests (it might take a while). + The `test:data` only needs to be run once and checks the data out. Read more about + test data [below](#test-data). + +- Run the viewer with e2e config + + ```bash + APP_CONFIG=config/e2e.js yarn start + ``` + + You should be able to see test studies in the study list + + ![OHIF-e2e-test-studies](../assets/img/OHIF-e2e-test-studies.png) + +- Open a new terminal inside the OHIF project, and run the e2e cypress test + + ```bash + yarn test:e2e + ``` + + You should be able to see the cypress window open + + ![e2e-cypress](../assets/img/e2e-cypress.png) + + Run the tests by clicking on the `Run #number integration tests` . + + A new window will open, and you will see e2e tests being executed one after + each other. + + ![e2e-cypress-final](../assets/img/e2e-cypress-final.png) + + ## Test Data + The testing data is stored in two OHIF repositories. The first contains the + binary DICOM data, at [viewer-testdata](https://github.com/OHIF/viewer-testdata.git) + while the second module contains data in the DICOMweb format, installed as a submodule + into OHIF in the `testdata` directory. This is retrieved via the command + ```bash + yarn test:data + ``` + or the equivalent command `git submodule update --init` + When adding new data, run: + ``` + npm install -g dicomp10-to-dicomweb + mkdicomweb -d dicomweb dcm + ``` + to update the local dicomweb submodule in viewer-testdata. Then, commit + that data and update the submodules used in OHIF and in the viewer-testdata + parent modules. + + All data MUST be fully anonymized and allowed to be used for open access. + Any attributions should be included in the DCM directory. + +## Testing Philosophy > Testing is an opinionated topic. Here is a rough overview of our testing -> philosiphy. See something you want to discuss or think should be changed? Open +> philosophy. See something you want to discuss or think should be changed? Open > a PR and let's discuss. You're an engineer. You know how to write code, and writing tests isn't all that @@ -22,7 +104,7 @@ Gaining the confidence we need to answer these questions after every change is costly. Good tests allow us to answer them without manual regression testing. What and how we choose to test to increase that confidence is nuanced. -## Kinds of Tests +## Further Reading: Kinds of Tests Test's buy us confidence, but not all tests are created equal. Each kind of test has a different cost to write and maintain. An expensive test is worth it if it @@ -84,10 +166,6 @@ We write integration tests to gain confidence that several units work together. Generally, we want to mock as little as possible for these tests. In practice, this means only mocking network requests. -#### When to use - -... - ### End-to-End Tests These are the most expensive tests to write and maintain. Largely because, when @@ -100,13 +178,6 @@ Mission critical features and functionality, or to cover a large breadth of functionality until unit tests catch up. Unsure if we should have a test for feature `X` or scenario `Y`? Open an issue and let's discuss. -## Summary - -- Does your test increase confidence? -- Does the test type chosen balance the cost-to-confidence ratio? - -## Further Reading - ### General - [Assert(js) Conf 2018 Talks][assert-js-talks] @@ -122,16 +193,13 @@ feature `X` or scenario `Y`? Open an issue and let's discuss. - [Best Practices](https://docs.cypress.io/guides/references/best-practices.html) - [Example Recipes](https://docs.cypress.io/examples/examples/recipes.html) -## Testing Dorito - -[![testing dorito][testing-dorito-img]][testing-dorito] - [eslint-rules]: https://eslint.org/docs/rules/ +[mini-pacs]: https://github.com/OHIF/viewer-testdata [typescript-docs]: https://www.typescriptlang.org/docs/home.html [flow-org]: https://flow.org/ diff --git a/platform/docs/versioned_docs/version-3.8/development/types.md b/platform/docs/versioned_docs/version-3.8/development/types.md new file mode 100644 index 000000000..a6af5df12 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/development/types.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 10 +sidebar_label: Global Types +--- + +# Extending App Types and Services in Your Application + +This documentation provides an overview and examples on how to use and extend `withAppTypes`, integrate custom properties, and add services in the global namespace of the application. This helps in enhancing the application's modularity and extensibility. + +## Overview of `withAppTypes` + +The `withAppTypes` function is a TypeScript utility that extends the base properties of components or modules with the application's core service and manager types. It allows for a more flexible and type-safe way to pass around core functionality and custom properties. + +### Using `withAppTypes` + +`withAppTypes` can be enhanced using generics to include custom properties. This is particularly useful for passing additional data or configurations specific to your component or service. + +### Extending with Custom Properties + +You can extend `withAppTypes` to include custom properties by defining an interface for the props you need. For example: + +```typescript +interface ColorbarProps { + viewportId: string; + displaySets: Array; + colorbarProperties: ColorbarProperties; +} + +export function Colorbar({ + viewportId, + displaySets, + commandsManager, // injected type + servicesManager, // injected type + colorbarProperties, +}: withAppTypes): ReactElement { + // Component logic here +} +``` + +In this example, `ColorbarProps` is a custom interface that extends the application types through `withAppTypes`. + +## Typing the custom extensions's new services + +Extensions can define additional services that integrate seamlessly into the application's global service architecture, and will be available on the ServicesManager for use across the application. + +### Adding the extension's services Types + +Declare your service in the global namespace and use it across your application as demonstrated below: + +`extensions/my-extension/src/types/whatever.ts` + +```typescript +declare global { + namespace AppTypes { + // only add if you need direct access to the service ex. AppTypes.MicroscopyService + export type MicroscopyService = MicroscopyServiceType; + // add to the global Services interface, and to withAppTypes + export interface Services { + microscopyService?: MicroscopyServiceType; + } + } +} +``` + +Doing the above adds the `microscopyService` to the global Services interface, which ServicesManager uses by default `public services: AppTypes.Services = {};` to type services, and is also used by withAppTypes to inject services into components. +You will also get access to the seperate services via `AppTypes.YourServiceName` in your application. + + +```typescript +export function CustomComponent({ + servicesManager, +}: withAppTypes): ReactElement { + const { microscopyService } = servicesManager.services; + microscopyService.someMethod(); // auto completation available + +} +``` + +```typescript +export function CustomComponent2( + microscopyService: AppTypes.MicroscopyService, +): ReactElement { + microscopyService.someMethod(); // auto completation available +} +``` diff --git a/platform/docs/versioned_docs/version-3.8/development/video-tutorials.md b/platform/docs/versioned_docs/version-3.8/development/video-tutorials.md new file mode 100644 index 000000000..482967e26 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/development/video-tutorials.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 4 +sidebar_label: Video Tutorials +--- + +# Video Tutorials + +## Creating, Linking and Publishing OHIF Modes and Extensions + +The [OHIF CLI](./ohif-cli.md) facilitates the creation, linkage and publication +of OHIF modes and extensions. The videos below walk through how to use the CLI for +- creating modes and extensions +- linking local modes and extensions +- publishing modes and extensions to NPM +- adding published modes and extensions to OHIF +- submitting a mode to OHIF + +The videos build on top of one another whereby the mode and extension created +in each of the first two videos are published to NPM and then the published +entities are added to OHIF. + +### Creating and Linking a Mode + +The first video demonstrates the creation and linkage of a mode. +
+ +
+ +### Creating and Linking an Extension + +The second video creates and links an extension. The mode from the first +video is modified to reference the extension. +
+ +
+ +### Publishing an Extension to NPM + +The third video shows how the extension created in the second video can +be published to NPM. +
+ +
+ +### Publishing a Mode to NPM + +The fourth video shows how the mode created in the first video can be +published to NPM. +
+ +
+ +### Adding a Mode from NPM + +The fifth video adds the mode and extension published in NPM to OHIF. Note +that since the mode references the extension both are added with one CLI +command. +
+ +
+ +### Submitting a Mode to OHIF + +The sixth video demonstrates how a mode can be submitted to OHIF to have it +appear in OHIF's mode gallery. +
+ +
diff --git a/platform/docs/versioned_docs/version-3.8/faq.md b/platform/docs/versioned_docs/version-3.8/faq.md new file mode 100644 index 000000000..5064899db --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/faq.md @@ -0,0 +1,302 @@ +--- +sidebar_position: 8 +sidebar_label: FAQ +--- + + +- [General FAQ](#general-faq) + - [How do I report a bug?](#how-do-i-report-a-bug) + - [How can I request a new feature?](#how-can-i-request-a-new-feature) + - [Who should I contact about Academic Collaborations?](#who-should-i-contact-about-academic-collaborations) + - [Does OHIF offer support?](#does-ohif-offer-support) + - [Does The OHIF Viewer have 510(k) Clearance from the U.S. F.D.A or CE Marking from the European Commission?](#does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission) + - [Is there a DICOM Conformance Statement for the OHIF Viewer?](#is-there-a-dicom-conformance-statement-for-the-ohif-viewer) + - [Is The OHIF Viewer HIPAA Compliant?](#is-the-ohif-viewer-hipaa-compliant) + - [Could you provide me with a particular study from the OHIF Viewer Demo?](#could-you-provide-me-with-a-particular-study-from-the-ohif-viewer-demo) +- [Technical FAQ](#technical-faq) + - [Why do I keep seeing a Cross Origin Isolation warning](#why-do-i-keep-seeing-a-cross-origin-isolation-warning) + - [What if my setup does not support the Shared Array Buffers API?](#what-if-my-setup-does-not-support-the-shared-array-buffers-api) + - [Viewer opens but does not show any thumbnails](#viewer-opens-but-does-not-show-any-thumbnails) + - [What are the list of required metadata for the OHIF Viewer to work?](#what-are-the-list-of-required-metadata-for-the-ohif-viewer-to-work) + - [Mandatory](#mandatory) + - [Optional](#optional) + - [How do I handle large volumes for MPR and Volume Rendering](#how-do-i-handle-large-volumes-for-mpr-and-volume-rendering) + - [`useNorm16Texture`](#usenorm16texture) + - [`preferSizeOverAccuracy`](#prefersizeoveraccuracy) + + +# General FAQ + + +## How do I report a bug? + +Navigate to our [GitHub Repository][new-issue], and submit a new bug report. +Follow the steps outlined in the [Bug Report Template][bug-report-template]. + +## How can I request a new feature? + +At the moment we are in the process of defining our roadmap and will do our best +to communicate this to the community. If your requested feature is on the +roadmap, then it will most likely be built at some point. If it is not, you are +welcome to build it yourself and [contribute it](development/contributing.md). +If you have resources and would like to fund the development of a feature, +please [contact us](https://ohif.org/get-support). + + +## Who should I contact about Academic Collaborations? + +[Gordon J. Harris](https://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/) +at Massachusetts General Hospital is the primary contact for any academic +collaborators. We are always happy to hear about new groups interested in using +the OHIF framework, and may be able to provide development support if the +proposed collaboration has an impact on cancer research. + +## Does OHIF offer support? + +yes, you can contact us for more information [here](https://ohif.org/get-support) + + +## Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission? + +**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users' +responsibility to ensure compliance with applicable rules and regulations. The +[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF +Platform does not prevent your company or group from seeking F.D.A. clearance +for a product built using the platform. + +If you have gone this route (or are going there), please let us know because we +would be interested to hear about your experience. + +## Is there a DICOM Conformance Statement for the OHIF Viewer? + +Yes, check it here [DICOM Conformance Statement](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) + +## Is The OHIF Viewer [HIPAA][hipaa-def] Compliant? + +**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA +Compliant. It is the users' responsibility to ensure compliance with applicable +rules and regulations. + +## Could you provide me with a particular study from the OHIF Viewer Demo? + +You can check out the studies that we have put in this [Dropbox link](https://www.dropbox.com/scl/fo/66xidsx13pn0zf3b9cbfq/ADaCgn7aT29WMlnTdT_WRXM?rlkey=rratvx6g4kfxnswjdbupewjye&dl=0) + +# Technical FAQ + + + +## Why do I keep seeing a Cross Origin Isolation warning +If you encounter a warning while running OHIF indicating that your application is not cross-origin isolated, it implies that volume rendering, such as MPR, will not function properly since they depend on Shared Array Buffers. To resolve this issue, we recommend referring to our comprehensive guide on Cross Origin Isolation available at [our dedicated cors page](./deployment/cors.md). + +## What if my setup does not support the Shared Array Buffers API? +You can simply disable that by adding the `useSharedArrayBuffer: 'FALSE'` (notice the string FALSE), and the volumes will only use a regular +array buffer which is a bit slower but will work on all browsers. + + +## Viewer opens but does not show any thumbnails + +Thumbnails may not appear in your DICOMWeb application for various reasons. This guide focuses on one primary scenario, which is you are using +the `supportsWildcard: true` in your configuration file while your sever does not support it. +One + +For instance for the following filtering in the worklist tab we send this request + +![](assets/img/filtering-worklist.png) + +`https://d33do7qe4w26qo.cloudfront.net/dicomweb/studies?PatientName=*Head*&limit=101&offset=0&fuzzymatching=false&includefield=00081030%2C00080060` + +Which our server can respond properly. If your server does not support this type of filtering, you can disable it by setting `supportsWildcard: false` in your configuration file, +or edit your server code to support it for instance something like + +```js +Pseudocode: +For each filter in filters: + if filter.value contains "*": + Convert "*" to SQL LIKE wildcard ("%") + Add "metadataField LIKE ?" to query + else: + Add "metadataField = ?" to query +``` + + + +## What are the list of required metadata for the OHIF Viewer to work? + + +### Mandatory + +**All Modalities** + +- `StudyInstanceUID`, `SeriesInstanceUID`, `SOPInstanceUID`: Unique identifiers for the study, series, and object. +- `PhotometricInterpretation`: Describes the color space of the image. +- `Rows`, `Columns`: Image dimensions. +- `PixelRepresentation`: Indicates how pixel data should be interpreted. +- `Modality`: Type of modality (e.g., CT, MR, etc.). +- `PixelSpacing`: Spacing between pixels. +- `BitsAllocated`: Number of bits allocated for each pixel sample. +- `SOPClassUID`: Specifies the DICOM service class of the object (though you might be able to render without it for most regular images datasets, but it is pretty normal to have it) + +**Rendering** + +You need to have the following tags for the viewer to render the image properly, otherwise you should +use the windowing tools to adjust the image to your liking: + +- `RescaleIntercept`, `RescaleSlope`: Values used for rescaling pixel values for visualization. +- `WindowCenter`, `WindowWidth`: Windowing parameters for display. + +**Some Datasets** + +- `InstanceNumber`: Useful for sorting instances (without it the instances might be out of order) + +**For MPR (Multi-Planar Reformatting) rendering and tools** + +- `ImagePositionPatient`, `ImageOrientationPatient`: Position and orientation of the image in the patient. + +**SEG (Segmentation)** + +- `FrameOfReferenceUID` for handling segmentation layers. +- sequences + - `ReferencedSeriesSequence` + - `SharedFunctionalGroupsSequence` + - `PerFrameFunctionalGroupsSequence` + +**RTSTRUCT (Radiotherapy Structure)** + +- `FrameOfReferenceUID` for handling segmentation layers. +- sequences + - `ROIContourSequence` + - `StructureSetROISequence` + - `ReferencedFrameOfReferenceSequence` + +**US (Ultrasound)** + +- `NumberOfFrames`: Number of frames in a multi-frame image. +- `SequenceOfUltrasoundRegions`: For measurements. +- `FrameTime`: Time between frames if specified. + +**SR (Structured Reporting)** + +- Various sequences for encoding the report content and template. + - `ConceptNameCodeSequence` + - `ContentSequence` + - `ContentTemplateSequence` + - `CurrentRequestedProcedureEvidenceSequence` + - `ContentTemplateSequence` + - `CodingSchemeIdentificationSequence` + +**PT with SUV Correction (Positron Tomography Standardized Uptake Value)** + +- Sequences and tags related to radiopharmaceuticals, units, corrections, and timing. + - `RadiopharmaceuticalInformationSequence` + - `SeriesDate` + - `SeriesTime` + - `CorrectedImage` + - `Units` + - `DecayCorrection` + - `AcquisitionDate` + - `AcquisitionTime` + - `PatientWeight` + +**PDF** + +- `EncapsulatedDocument`: Contains the PDF document. + +**Video** + +- `NumberOfFrames`: Video frame count . + + +### Optional +There are various other optional tags that will add to the viewer experience, but are not required for basic functionality. These include: +Patient Information, Study Information, Series Information, Instance Information, and Frame Information. + + +## How do I handle large volumes for MPR and Volume Rendering + +Currently there are two ways to handle large volumes for MPR and Volume Rendering if that does not +fit in the memory of the client machine. + +### `useNorm16Texture` + +WebGL officially supports only 8-bit and 32-bit data types. For most images, 8 bits are not enough, and 32 bits are too much. However, we have to use the 32-bit data type for volume rendering and MPR, which results in suboptimal memory consumption for the application. + +Through [EXT_texture_norm16](https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/) , WebGL can support 16 bit data type which is ideal +for most images. You can look into the [webgl report](https://webglreport.com/?v=2) to check if you have that extension enabled. + +![](assets/img/webgl-report-norm16.png) + + +This is a flag that you can set in your [configuration file](./configuration/configurationFiles.md) to force usage of 16 bit data type for the volume rendering and MPR. This will reduce the memory usage by half. + + +For instance for a large pt/ct study + +![](assets/img/large-pt-ct.png) + +Before (without the flag) the app shows 399 MB of memory usage + +![](assets/img/memory-profiling-regular.png) + + +After (with flag, running locally) the app shows 249 MB of memory usage + + +![](assets/img/webgl-int16.png) + +:::note +Using the 16 bit texture (if supported) will not have any effect in the rendering what so ever, and pixelData +would be exactly shown as it is. For datasets that cannot be represented with 16 bit data type, the flag will be ignored +and the 32 bit data type will be used. + + +Read more about these discussions in our PRs +- https://github.com/Kitware/vtk-js/pull/2058 +::: + + +:::warning +Although the support for 16 bit data type is available in WebGL, in some settings (e.g., Intel-based Macos) there seems +to be still some issues with it. You can read and track bugs below. + +- https://bugs.chromium.org/p/chromium/issues/detail?id=1246379 +- https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 +::: + +### `preferSizeOverAccuracy` + +This is another flag that you can set in your [configuration file](./configuration/configurationFiles.md) to force the usage of the `half_float` data type for volume rendering and MPR. The main reason to choose this option over `useNorm16Texture` is its broader support across hardware and browsers. However, it is less accurate than the 16-bit data type and may lead to some rendering artifacts. + +```js +Integers between 0 and 2048 can be exactly represented (and also between −2048 and 0) +Integers between 2048 and 4096 round to a multiple of 2 (even number) +Integers between 4096 and 8192 round to a multiple of 4 +Integers between 8192 and 16384 round to a multiple of 8 +Integers between 16384 and 32768 round to a multiple of 16 +Integers between 32768 and 65519 round to a multiple of 32 +``` + +As you see in the ranges above 2048 there will be inaccuracies in the rendering. + +Memory snapshot after enabling `preferSizeOverAccuracy` for the same study as above + +![](assets/img/preferSizeOverAccuracy.png) + + + + + +[general]: #general +[technical]: #technical +[report-bug]: #how-do-i-report-a-bug +[new-feature]: #how-can-i-request-a-new-feature +[commercial-support]: #does-ohif-offer-commercial-support +[academic]: #who-should-i-contact-about-academic-collaborations +[fda-clearance]: #does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission +[hipaa]: #is-the-ohif-viewer-hipaa-compliant +[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/ +[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en +[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act +[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose +[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title= diff --git a/platform/docs/versioned_docs/version-3.8/migration-guide/_category_.json b/platform/docs/versioned_docs/version-3.8/migration-guide/_category_.json new file mode 100644 index 000000000..e4b82ff55 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/migration-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Migration Guides", + "position": 11 +} diff --git a/platform/docs/versioned_docs/version-3.8/migration-guide/from-3p7-to-3p8.md b/platform/docs/versioned_docs/version-3.8/migration-guide/from-3p7-to-3p8.md new file mode 100644 index 000000000..dde25c341 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/migration-guide/from-3p7-to-3p8.md @@ -0,0 +1,142 @@ +--- +sidebar_position: 1 +sidebar_label: 3.7 -> 3.8 +--- + +# Migration Guide + +There are two main things that need to be taken care of. + + +## New Toolbar Button definitions + +### Update Active Tool Handling +The concept of `activeTool` and its associated getter and setter has been removed. The active tool should now be derived from the toolGroup and the viewport. + + +**Action Needed** + +Remove any code that sets the default tool using `toolbarService.setDefaultTool()` and activates the tool using +`toolbarService.recordInteraction()`. For example, the following code should be removed: + +```javascript +let unsubscribe; +toolbarService.setDefaultTool({ + groupId: "WindowLevel", + itemId: "WindowLevel", + interactionType: "tool", + commands: [ + { + commandName: "setToolActive", + commandOptions: { + toolName: "WindowLevel", + }, + context: "CORNERSTONE", + }, + ], +}); + +const activateTool = () => { + toolbarService.recordInteraction(toolbarService.getDefaultTool()); + + unsubscribe(); +}; + +({ unsubscribe } = toolGroupService.subscribe( + toolGroupService.EVENTS.VIEWPORT_ADDED, + activateTool +)); +``` + + + +Instead, focus on defining the buttons and their placement in the toolbar using `toolbarService.addButtons()` and `toolbarService.createButtonSection()`. For example: + +```javascript +toolbarService.addButtons([...toolbarButtons, ...moreTools]); +toolbarService.createButtonSection("primary", [ + "MeasurementTools", + "Zoom", + "WindowLevel", + "Pan", + "Capture", + "Layout", + "MPR", + "Crosshairs", + "MoreTools", +]); +``` + + +### Update Button Definitions +The concept of button types (toggle, action, tool) has been removed. Buttons are now defined using a simplified object-based definition. + +**Action Needed** + +Update your button definitions to use the new object-based format and remove the `type` property. Use the `uiType` property for the top-level UI type definition. For example: + +```javascript +// Old Implementation +{ + id: 'Capture', + type: 'ohif.action', + props: { + icon: 'tool-capture', + label: 'Capture', + type: 'action', + commands: [ + { + commandName: 'showDownloadViewportModal', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }, +}, +``` + +is now + +```javascript +// New Implementation +{ + id: 'Capture', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: [ + { + commandName: 'showDownloadViewportModal', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }, +}, +``` + +### Add Evaluators to Button Definitions +Introduce the evaluate property in your button definitions to determine the state of the button based on the app context. + +**Action Needed** + +Add the appropriate `evaluate` property to each button definition. For example: + - Use `evaluate.cornerstoneTool` if the button should be highlighted only when it is the active primary tool (left mouse). + - Use `evaluate.cornerstoneTool.toggle` if the tool is a toggle tool (like reference lines or image overlay). + +Refer to the `modes/longitudinal/src/toolbarButtons.ts` file for examples of using the `evaluate` property. + +Additional Resources + + - For more information on the new toolbar module and its usage, refer to the [Toolbar documentation](../platform/extensions/modules/toolbar.md). + - Consult the updated button definitions in `modes/longitudinal/src/toolbarButtons.ts` for examples of the new object-based button definition format and the usage of evaluators. + +## leftPanelDefaultClosed and rightPanelDefaultClosed + +Now they are renamed to `leftPanelClosed` and `rightPanelClosed` respectively. + + +## StudyInstanceUID in the URL param + +Previously there were two params that you could choose: seriesInstanceUID and seriesInstanceUIDs, they have been replaced with seriesInstanceUIDs so even if you would like to filter one series use ``seriesInstanceUIDs` diff --git a/platform/docs/versioned_docs/version-3.8/migration-guide/from-v2.md b/platform/docs/versioned_docs/version-3.8/migration-guide/from-v2.md new file mode 100644 index 000000000..20ee1afeb --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/migration-guide/from-v2.md @@ -0,0 +1,996 @@ +--- +sidebar_position: 2 +sidebar_label: 2.x -> 3.5 +--- + +# Migration Guide + +On this page, we will provide a guide to migrating from OHIF v2 to v3. Please note +that this document is a work in progress and will be updated as we move forward. +This document is not meant to be used as a migration recipe but as a migration overview. + + +# Introduction + +## Importance of Migration + +- Enhanced UX: the new design and UI of OHIF v3 provides a more intuitive and user-friendly experience. + OHIF v3 adds an improved side panels and toolbar, and a new layout system that lets you customize + the layout of your application. +- Improved Performance: OHIF v3 leverages the new Cornerstone3D rendering and tooling libraries, which + significantly improve performance and provide a more robust and stable foundation for your application + for rendering and interacting with medical images. Some of the new advanced features in Cornerstone3D + include: OffScreen Rendering and GPU Acceleration for all viewports, streaming of the volume data, 3D annotations and measurements, + sharing tool states between viewports and more. +- Improved Customizability: With addition of Modes and Extensions, OHIF v3 provides a more modular + and customizable framework for building medical imaging applications, this will let you + focus on your use case and not worry about the underlying infrastructure and also have less worry + to keep up to date with the latest changes. +- Community driven Modes: OHIF v3 provides a gallery of modes that you can use as a starting point + for your application. These +- Future-Proofing: By migrating to v3, you align your application with the latest advancements in the OHIF framework, ensuring ongoing support, updates, and access to new features. +- Community Support: OHIF v3 benefits from an active community of developers and contributors who provide valuable support, bug fixes, and continuous improvements. + +## Migration Timeframe + +The duration of the migration process can vary depending on factors such as the complexity of custom changes made in v2, familiarity with v3's architecture, and the size of the codebase. +If you don't have any custom changes in v2, the migration process should be relatively straightforward. If you have custom changes, you will need to update them to work with the new architecture and new +rendering and tooling engines. + + +## Complexity and Pain Points + +Certain scenarios can make the migration process more complex and potentially introduce pain points: + +- Extensive Customizations: If your v2 implementation includes extensive custom changes and overrides, adapting those customizations to the new structure and APIs of v3 may require additional effort and careful refactoring. +- UI Customizations: Since in OHIF v3 we moved our component library to Tailwind CSS + if you have any custom UI components, you will need to migrate them to Tailwind CSS too, and this might be a bit time consuming. +- Hardware requirements: Since Cornerstone3D uses WebGL for rendering volumeViewport (although it has + a CPU rendering fallback), you need to make sure that your target hardware supports WebGL. You can check + if your hardware supports WebGL [here](https://get.webgl.org/). Also regarding the GPU requirements, you can check the tier of your GPU [here](https://pmndrs.github.io/detect-gpu/), if it is tier 1 and above, you + should be good to go. + +## Summary of Changes + +OHIF v3 is a major re-architecture of the OHIF v2 to make it more modular and +easier to maintain. The main differences are: + +- platform/viewer (@ohif/viewer) has been renamed to platform/app (@ohif/app) (explanation below) +- Extensions are available to be used by modes on request, but are still injected as module components. +- To use the modules provided by the extensions, you need to write a [Mode](../platform/modes/index.md). Modes +are configuration objects that will be used by the viewer to load the modules. This lets users to be able to use common extensions with different configurations, and enhances the customizability of the viewer. +- App configuration structure is different, mainly the `servers` is renamed to `dataSources`. +- Apps can be customized significantly more than previously by providing configuration code int he customizationModule section. +- The viewer UI is completely re-written in Tailwind CSS for better maintainability, although it is a WIP but + already provides a better user experience. +- cornerstone-core and cornerstone-tools are removed and OHIF v3 is using the new Cornerstone3D rendering library and tools. Moving to Cornerstone3D has enabled us to provide a more robust and stable foundation + for 3D rendering and 3D annotations and measurements. In addition, Cornerstone3D provides APIs to load + and stream data into a volume which has huge performance benefits. +- A new CLI tool to help you create extensions and modes (more [here]((./development/ohif-cli.md))) +- redux store has been removed and replaced with a simpler state management system via React Context API. + +New significant additions that might be useful for you that weren't available in OHIF v2: +- [OHIF CLI](../development/ohif-cli.md) +- [New Rendering Engine and Toolings](https://www.cornerstonejs.org/) +- [Modes](../platform/modes/index.md) +- [Mode Gallery](https://ohif.org/modes) +- [Layouts](../platform/extensions/modules/layout-template.md) +- [Data Sources](../platform/extensions/modules/data-source.md) +- [Hanging Protocols](../platform/services/data/HangingProtocolService.md) +- [URL Params](../configuration/url.md) + +## Platform/viewer (@ohif/viewer) -> platform/app (@ohif/app) + + +To ensure proper versioning of OHIF v3, we have made a decision to rename the platform/viewer to platform/app. Previously, the platform/viewer package followed software engineering versioning (currently at v4.12.51). However, going forward, we aim to align the versioning of platform/app with the product version (e.g., v3.4.0, v3.5.0, etc.). + +Since the platform/viewer (@ohif/viewer) is already at v4.12.51, we opted to rename it as platform/app to enable versioning in accordance with the product versioning approach. If you were utilizing any exports from @ohif/viewer, please update them to use @ohif/app instead. + + +## Configuration + +:::tip +There are various configurations available to customize the viewer. Each configuration is represented by a custom-tailored object that should be used with the viewer to work effectively with a specific server. Here are some examples of configuration files found in the platform/app/public/config directory. Some server-specific configurations that you should be aware are: `supportsWildcard`, `bulkDataURI`, `omitQuotationForMultipartRequest`, `staticWado` (Read more about them [here](../configuration/configurationFiles.md)). + +- default.js: This is our default configuration designed for our main server, which uses a Static WADO datasource hosted on Amazon S3. +- local_orthanc.js: Use this configuration when working with our local Orthanc server. +- local_dcm4chee.js: This configuration is intended for our local dcm4chee server. +- netlify.js: This configuration is the same as default.js and is used for deployment on Netlify. +- google.js: Use this configuration to run the viewer against the Google Health API. +::: + +OHIF v3 has a new configuration structure. The main difference is that the `servers` is renamed to `dataSources` and the configuration is now asynchronous. Datasources are more abstract and +far more capable than servers. Read more about dataSources [here](../platform/extensions/modules/data-source.md). + +- `StudyPrefetcher` is not currently supported in OHIF v3. +- The `servers` object has been replaced with a `dataSources` array containing objects representing different data sources. +- The cornerstoneExtensionConfig property has been removed, you should use `customizationService` instead (you can read more [here](../platform/services/ui/customization-service.md)) +- The maxConcurrentMetadataRequests property has been removed in favor of `maxNumRequests` +- The hotkeys array has been updated with different command names and options, and some keys have been removed. +- New properties have been added, including `maxNumberOfWebWorkers`, `omitQuotationForMultipartRequest`, `showWarningMessageForCrossOrigin`, `showCPUFallbackMessage`, `showLoadingIndicator`, `strictZSpacingForVolumeViewport`. +- you should see if `supportsWildcard` is supported in your server, some servers don't support it and you need to make it false. + +## Modes + +As mentioned briefly above, modes are configuration objects that will be used by the viewer to load extensions. +This lets users to be able to use common extensions with different configurations. So as OHIF developers can focus on creating extensions while +you as the user can focus on creating modes having your own use case and configuration/initialization logic in mind. + +Separating the configuration from the extensions also makes it so that you can +have multiple modes in a single application each focusing on certain tasks. For example, you can have a mode for segmentation which uses specific panels and tools which you don't need +for a mode that will be used for reading (read more about modes [here](../platform/modes/index.md)) + +:::info +Previously, the viewer was designed around registered extensions. If you had a specific use case, you had to duplicate the viewer code and incorporate your customizations through extensions. However, with the introduction of a new layer of abstraction called Modes, you no longer need to fork the viewer. + +Modes provide a flexible approach where you can create your own mode and utilize the necessary extensions within that mode. This eliminates the need for duplicating the viewer codebase. + +Furthermore, Modes offer the advantage of having multiple applications within a single viewer. For instance, you can have a mode dedicated to segmentation tasks and another mode focused on reading. Each mode can have its own unique configuration, initialization logic, layout, tools, and hanging protocols. This ensures a cleaner user interface in the viewer and an improved user experience overall. +::: + +Upon entering a mode, the Viewer will register its declared extensions and load them. And you +can specify which modules you need from each extension in the mode configuration. For instance + +```js + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + measurements: '@ohif/extension-default.panelModule.measure', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const cs3d = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const tmtv = { + hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', + petSUV: '@ohif/extension-tmtv.panelModule.petSUV', + ROIThresholdPanel: '@ohif/extension-tmtv.panelModule.ROIThresholdSeg', +}; + +function modeFactory({ modeConfiguration }) { + routes: [ + { + path: 'tmtv', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + // leftPanels: [ohif.thumbnailList], + rightPanels: [tmtv.ROIThresholdPanel, tmtv.petSUV], + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], +} +``` + +In the example above, we are using the `tmtv` mode which is a mode for reading PET/CT scans +and as you can see we are specifying the layout, the panels and the viewports that we need +for this mode. The `tmtv` mode is using the `cs3d` extension for rendering and the `ohif` extension. As you see you can reference the modules from the extensions using the `namespace` via strings. So for instance, if you need to use the `viewportModule` from the `@ohif/extension-cornerstone` you can use `@ohif/extension-cornerstone.viewportModule.cornerstone` as the namespace. + +:::tip +`ExtensionManager` will register and load the modules from the extensions and make them available to the viewer by their namespaces. +::: + +Below you can see a screen shot from the demo showcasing 3 modes for the opened study. + +![Alt text](../assets/img/migration-modes.png) + +:::tip +How do I decide certain thing should go inside a mode or extension, Here are some considerations to help you make the decision: + +- **Functionality Scope**: If the functionality is specific to a particular use case or task within your viewer, it is often best suited to be included within a mode. Modes allow you to create customized configurations, layouts, panels, tools, and other components specific to a particular task or workflow. This includes which tool to be active by default, which panels to be displayed, and which layout to be used. + +- **Reusability**: If the functionality can be used across multiple modes, it is better to implement it as an extension. Extensions provide a modular approach where you can encapsulate and share functionality across different modes. For instance, if you have a custom panel that you want to use in multiple modes, you can implement it as an extension and include it in + the mode configuration. + +- **Complexity**: If the functionality requires significant customizations, complex logic, or extensive modifications to the viewer's core behavior, it might be better suited as an extension. + +- **New Service**: If you are writing a new service, it is preferable to implement it as an extension. Services are used to provide a common interface for interacting with external systems and data sources. +There is a new way to register new services which are extendible by other extensions. + +Remember that there is no strict rule for deciding between modes and extensions. It's a matter of understanding the specific requirements of your application. + +::: + + +## Routes + +In OHIF v2 a study was loaded and mounted on `/viewer/:studyInstanceUID` route. In OHIF v3 +we have reworked the route registration to enable more sophisticated routing. Now, Modes are tied to specific routes in the viewer, and multiple modes/routes can be present within a single application, making "routes" configuration the most important part of mode configuration. + +- Routes with a dataSourceName: `${mode.id}/${dataSourceName}` +- Routes without a dataSourceName: `${mode.id}` which uses the default dataSourceName + +This makes a mode flexible enough to be able to connect to multiple datasources +without rebuild of the app for use cases such as reading from one PACS and +writing to another. + +
+ +Can I register a custom route to OHIF v3? + + +Yes, you can take advantage of the customizationService and register your own routes. +see [custom routes](../platform/services/ui/customization-service.md#customroutes) + + +
+ + +## DICOM Endpoints + +In OHIF v3 there is a new end point that your DICOM server should be able to respond to +`WADO-RS GET studies/{studyInstanceUid}/series` + +This is used in the viewer for fetching the series list for a study to use for the hanging protocol. + +## LifeCycle Hooks + +OHIF v2 had `preRegistration` hook for extensions for initialization. In OHIF v3 you have +even more control using `onModeEnter` and `onModeExit` hooks on the extensions and on the modes. + +- `preRegistration`: is called before the extension is registered to the viewer. So very early in the lifecycle of the viewer. +- `onModeEnter` is called when the mode is entered (component on the route is mounted, e.g., when you click on the mode to enter it) +- `onModeExit` is called when the mode is exited (component on the route is unmounted, e.g., when you navigate back to the worklist) + +## Extensions + +Since extensions in OHIF v2 were the main way of customizing the viewer, we will spend some time +below to explain how you can migrate your extensions to OHIF v3. + +### Default Extension + +Lots of common functionalities in the platform/core has been moved inside +the `@ohif/extension-default` extension. This extension is loaded by default +in the viewer and it provides the following functionalities: + +- common datasources such as DICOMWeb, DICOMLocal, and DICOMJSON datasource. +- default measurement panel and panel study browser +- common toolbar button layouts +- common hanging protocol configurations + +
+ +how can I integrate to my google health api? is there support for that? + + +You can right now, take a look into our google configuration that we use for our QA located at +`config/google.js`. Also we have some exciting UI changes coming up for the next release +that will make it easier to integrate with google health api. +
+ +
+ +Is there any recommendation for PACS integration + + +You can take a look at open source PACS such as dcm4chee or orthanc. We have support for them. Also +we have a new static wado datasource that you can use to take benefit of new deduplicated metadata +and caching features. + +
+ +### Cornerstone Extension + +In OHIF v2, the Cornerstone extension provided modules like Cornerstone ViewportModule, ToolbarModule, and CommandsModule for controlling viewport actions. +It relied on `react-cornerstone-viewport` for rendering viewports, `cornerstone-tools` for tools, and `cornerstone-core` for core functionalities. + +However, in OHIF v3, there have been significant changes. The rendering and tooling logic has been migrated to a new library called [`Cornerstone3D`](https://github.com/cornerstonejs/cornerstone3D-beta/). This means that all viewport rendering and tool functionalities are now handled by Cornerstone3D. + +Additionally, in OHIF v3, the native support for 3D functionalities previously provided by the `vtk` extension has been integrated into Cornerstone3D. As a result, any vtkjs logic is encapsulated on CS3D. Things now are much more cleaner and simpler. + +To migrate from OHIF v2 to OHIF v3: + +#### Loading + +Previously we used `cornerstone-wado-image-loader` for loading images. However, we have fully switched the a new +library called `@cornerstonejs/dicom-image-loader` which is a fork of `cornerstone-wado-image-loader` with typescript support and bug fixes. +We have deprecated `cornerstone-wado-image-loader` and you should also switch to `@cornerstonejs/dicom-image-loader` as well. +The process is very simple, you can follow this [PR](https://github.com/OHIF/Viewers/pull/3339) to see how we have migrated. + +There is also a new loader and package `@cornerstonejs/streaming-image-volume-loader`, which provides streaming of the image data +into a volume using web workers and web assembly. You can look into the cornerstone documentation and read more about the +volumeViewport and volumeLoader. + + +#### Rendering + +The significant difference between cornerstone-core and cornerstone3D is that cornerstone3D fully utilizes +[vtk.js](https://kitware.github.io/vtk-js/) for rendering, however in cornerstone-core we used a mix of webGL and vtk +for rendering. While you don't need to do a migration for this, you should be aware that the rendering is now fully performed in the +world coordinate system and the image is placed in the world coordinate system using the `imagePositionPatient` and `imageOrientationPatient` +attributes of the image. This means that you can now share the tool states between multiple viewports and you can also +use the same tool states for 2D and 3D viewports. + +:::tip + +In OHIF v3, we have removed the OHIF's vtk extension and migrated all the 3D functionalities to Cornerstone3D. + +Also you need to remove any dependencies on `react-cornerstone-viewport`, `cornerstone-tools`, and `cornerstone-core`. +::: + +#### Tools + +If you don't have any custom tools, you most likely won't need to make any changes as have tried +to migrate all the tools from `cornerstone-tools` to Cornerstone3D (except `ROIWindowLevel` which is work in progress right now). + +Cornerstone3D has moved the coordinate system of tools to the world coordinate system enabling sharing +tool states between multiple viewports, and as a result the toolData is now stored in the world coordinate system as well. +So to migrate your tools, you will need to update your toolData to be stored in the world coordinate system. You can look +into the simplest tool for instance LengthTool in both `cornerstone-tools` and `cornerstone3D` to see the difference. + + + +By following these steps, you can leverage the improved rendering and tooling capabilities of Cornerstone3D and eliminate the need for the old ohif's vtk extension in OHIF v3. + + +
+ +Is there any name standard for modes and extensions? + + +No naming standard, you can have your organization name as a prefix for your modes and extensions as we +do for ohif (`@ohif/extension-*` and `@ohif/mode-*`). + +
+ + +
+ +What happens if I have create a mode with same name as existing one + + +You shouldn't. Modes are configuration objects that you can simply. There is no real use case +for creating a mode with same name as existing one. If you do so, the last one will override the previous one + + +
+ + +
+ +How to remove an "core" extension/mode? + + +You can use the OHIF cli tool to add/remove/link and unlink extensions and modes. You can find more information +about the cli tool [here](../development/ohif-cli.md) + +
+ +
+ +If I have vtkjs implementation how can I port it? Should I create a specific extension for that? + + +Cornerstone3D has support for some vtk.js actor and mappers including imageData, polyData and volume. If you have another +implementation of vtk.js actor or mapper, you might be able to use `viewport.addActor` to include it in the rendering +pipeline, but depending on the implementation and how much it interfere with the cornerstone3D rendering pipeline, you might +not get the expected result. + +
+ + + +### DICOM Segmentation & DICOM RT + +In OHIF v3, the equivalent extensions for RT and SEG exists with similar logic, but with various improvements such as +enhanced ui/ux for segmentation panel, faster loading and interaction, and better support for multiple viewports, +animations for jump to segment, volumetric rendering, and more. Additionally, OHIF v3 introduces new functionalities with the SEG Viewport and RT Viewport. + +:::tip + +In OHIF v3, Segmentation objects +are loading using the frame of reference by default which means that if there are two viewports that are using the same frame of reference, +if you load a segmentation (labelmap or RT) which lives in the same frame of reference, it will be loaded in both viewports. +::: + +When loading a series that contains SEG (Segmentation) or RT (RT Structure Set) data, the viewport will automatically +switch to the corresponding SEG or RT viewport. The user will then be prompted to decide whether to load the segmentation +or RT structure set into the viewer. This new feature addresses a common use case in which there are multiple segmentation +series in a study, and the user only wants to load specific ones. In OHIF v3, the Segmentations are all loaded +as 3D volumes and as a result a volume viewport is used to display them. (Stack Segmentation in Cornerstone3D is still a +work in progress.) + +In OHIF v2, the user had to load all the segmentation series and then manually delete the ones they didn't want to see. +However, in OHIF v3, the user has more control. The temporary SEG or RT viewport does not immediately load (hydrate) +the segmentation or RT structure set. Instead, the user can decide which ones to load, reducing unnecessary +loading and providing a more efficient workflow. + +This enhancement in OHIF v3 allows users to selectively load specific segmentations or RT structure sets, +improving the usability and efficiency of the viewer when working with multiple SEG or RT series. + + + + +
+ +Can I load one seg in one viewport and another in another viewport? + + +If there is another viewport in the grid that is using the same frame of reference, the segmentation will be loaded in that viewport as well. + +However, since we split the concept of `load` (`hydration`) and `preview`, you can use the preview (not load), which +makes sure the SEG is contained within the viewport, but it is not hydrated so you cannot edit it. + +In future however, we will add more controls over, hiding the segmentation in other viewports via UI, however, you can +right now do it via code. + + + +
+ +
+ +Does it support nifti? + + +Nifit support for both image and segmentation is coming soon. We are working on it. + + +
+ +### DICOM SR + +In OHIF v2, DICOM SR functionality was integrated into the Cornerstone extension. However, in OHIF v3, DICOM SR is now a separate extension. The DICOM SR extension in OHIF v3 retains the same loading and hydrating logic using dcmjs adapters. Additionally, it introduces a new type of viewport called the SR Viewport, which is used to display SR data. + +Similar to the temporary SEG and RT viewports, when a SR display set is selected in OHIF v3, the user is prompted to decide whether to load the SR data into the viewer and initiate the tracking. The SR viewport allows the user to switch between different measurements within the SR instance by utilizing the arrow buttons located at the top of the viewport. + +:::tip +This separation of DICOM SR into its own extension in OHIF v3 provides a dedicated viewport type for SR data and offers enhanced functionality for interacting with SR measurements within the viewer. +::: + + +### DICOM Tag Browser + +In OHIF v2, the DICOM Tag Browser was a separate extension that provided a dedicated user interface for exploring DICOM tags. However, in OHIF v3, we have integrated the DICOM Tag Browser functionality into the `default` extension. + +The DICOM Tag Browser is a powerful tool for debugging and inspecting DICOM metadata, and we wanted to make it easily accessible to users. As a result, it is now available as a toolbar icon within the `default` extension. This allows users to conveniently access the DICOM Tag Browser directly from the toolbar, eliminating the need for a separate extension. + + +
+ +Now that dicom tag is integrated back to default extension, how can I port my code that was implemented in the old extension? Should I create an extension or change directly into default? + + +If you have a custom tag browser, you have two options, either modify the default tag browser (if you think the features +you added is useful for everyone, feel free to open a PR!), or create your own extension with your custom tag browser +which then you can add to the toolbar. + + + +
+ + +### DICOM HTML + +Since we have added graphical overlay of DICOM SR in OHIF v3, we have temporarily downgraded the priority of displaying DICOM HTML within the viewer. While DICOM HTML support is not available in the current version of OHIF v3, we acknowledge its importance and plan to reintroduce this functionality in future updates. + + + +
+ +is there any easy way for supporting my own dicom html viewer? Should I use extension? + + +Yes, you can write your own sopClassHandler and custom viewport in your custom extensions. +After, you need to associate that with the viewport that you +will use in the mode configuration, this way when that sopClassUID is requested it will use your custom viewport. + + + +
+ + + +### DICOM Microscopy + +In OHIF v2, the DICOM microscopy engine was based on an older version of the [DICOM microscopy viewer](https://github.com/ImagingDataCommons/dicom-microscopy-viewer) maintained by our friends at IDC (Imaging Data Commons). However, in OHIF v3, we have upgraded to the latest version of the DICOM microscopy viewer. This new version offers significant improvements in terms of robustness and performance, providing users with an enhanced microscopy viewing experience. + +One notable addition in the latest DICOM microscopy viewer is the support for annotations within the whole slide images (SM images). This feature allows users to annotate and mark specific regions of interest directly within the microscopy images. + +:::tip +Looking ahead, our future plans include adding DICOM SR (Structured Reporting) support for export of annotations in microscopy images. While we will enhance our support for SM images (color profiles etc.), we recommend utilizing the [SLIM Viewer](https://github.com/ImagingDataCommons/slim) developed by IDC for more sophisticated microscopy use cases. +::: + + + +## Extension Modules + + +v3 Extension is likely the same as in v2. Extensions can (like before) have +modules exported via `get{ModuleName}Module` (e.g., `getViewportModule`). + +:::info +There are new +types of modules that can be exported from extensions (such as `HangingProtocolModule`, `LayoutModule`, read more about +modules in v3 [here](../platform/extensions/index.md)). +::: + +The main difference between v3 and v2 is that exported modules were represented as a single object, whereas in OHIF v3, they are +represented as an array of objects, each having a name property. This change was implemented to +enable extensions to export multiple named submodules, providing more flexibility and modularity. + +To access these modules in OHIF v3, you can use the namespace provided by the `ExtensionManager`. For example, consider the following code snippet + + +```js +getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + dicomLoaderService, + registerColormap, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + ]; +}, +``` + + +In this example, the extension is exporting multiple submodules named 'common', +'core', and 'tools'. To access the 'common' submodule provided by the @ohif/extension-cornerstone extension, +you can use the following code: + +```js +extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' +); +``` + +This allows you to access the specific submodule provided by the extension and utilize its functionalities within your application. + + +
+ +How can I have a lazy-loaded component and import it from another extension? + + +If an extension is exporting a component, you can import it from another extension. For example, if you have an extension that exports a component called `MyComponent`, you can import it from another extension like this: + +```js +import { MyComponent } from '@ohif/extension-my-extension'; +``` + + +
+ + +### ToolbarModule + +In OHIF v2, the toolbarModule was used to add buttons to the toolbar. For example, the following code snippet demonstrates adding a zoom tool button to the toolbar: + +In OHIF v2 + +```js +{ + id: 'Zoom', + label: 'Zoom', + icon: 'search-plus', + // + type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, +}, +``` + +However, in OHIF v3, the toolbarModule has been repurposed to define different button types. For instance, OHIF v3 introduces the ohif.radioGroup and ohif.splitButton button types, which provide more flexibility in defining toolbar buttons for each mode. + + + +```js +{ + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + clickHandler: () => {}, +}, +{ + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, +}, +``` + +To use these button types within your modes, you can define the buttons in your mode's configuration. In the onModeEnter hook, you can add the defined buttons to the toolbar using the toolbarService. Here's an example of how to add buttons to the toolbar: + + + +```js +// toolbar button +{ + id: 'Zoom', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-zoom', + label: 'Zoom', + commands: _createSetToolActiveCommands('Zoom'), + }, +}, +``` + +and in `onModeEnter` + +```js +onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + const { + toolbarService, + toolGroupService, + } = servicesManager.services; + + // Init tool groups (see cornerstone3D for more details) + initToolGroups(extensionManager, toolGroupService, commandsManager); + + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); +}, +``` + +By using the updated toolbarModule in OHIF v3, you can define and add toolbar buttons specific to each mode, providing greater flexibility and customization options for the toolbar configuration. + +An example of split button icon in v3 is shown below + +![Alt text](../assets/img/migration-split-button.png) + +
+ +Is the tool state shared between two different modes? + +No, the tool state is not shared between different modes in OHIF v3. Each mode operates independently and maintains its own tool state. + +
+ +
+ +I have a custom icon. How can I add it to the toolbar? + + +You need to first register it via `addIcon` in the src/components/Icon, and then you can +referenced it by name in the toolbar configuration for mode +
+ + +
+ +Can I change the toolbar's location? Can I add a secondary toolbar? + +Not in our default layout, but you can write your own layout in your custom extension +and use it instead of the default one. + +
+ +
+ +Can I have different tool sets for each viewport? + + +We don't have fully support for this yet, but we have plans for it. Basically, the plan +is to use the viewport action bar in the top of the viewport to provide viewport-specific +tool sets. +
+ +
+ +Are all tools from v2 support in v3? + + +Almost all with the exception of ROIWindow, but we have plans to add it in the future. However, there are +much more tools in v3 that are not available in v2 such as referenceLines, Stack Image Sync, and +Calibration tool. +
+ +### CommandsModule + +The structure of the commands module is the same as before. The only difference is that +we use Cornerstone3D for rendering and tools. So, if you have a custom command that you were +using in the v2, you need to migrate it to the new Cornerstone3D API. + +You can visit the migration guide for cornerstone [here](https://www.cornerstonejs.org/docs/migrationGuides). + +### PanelModule + +Previously in OHIF v2 you had + +```js +return { + menuOptions: [ + { + icon: 'list', + label: 'Segmentations', + target: 'segmentation-panel', + stateEvent: SegmentationPanelTabUpdatedEvent, + }, + ], + components: [ + { + id: 'segmentation-panel', + component: ExtendedSegmentationPanel, + }, + ], + defaultContext: ['VIEWER'], +}; +``` + +but in OHIF v3 you have + +```js +return [ + { + name: 'panelSegmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentation, + }, +]; +``` + +
+ +How can I add my own custom panel? + +To add your own custom panel in OHIF v3, you can follow these steps: + +- Create a new React component that represents your custom panel. +- Provide it in the getPanelModule of your extension. +- Inside your mode, add the panel namespace to the mode's configuration for the layout module. + +
+ +
+ +How to enhance an existing panel? + +To enhance an existing panel in OHIF v3, you can create a new React component that extends or wraps the existing panel component. In your enhanced component, you can add additional functionality, modify the appearance, or incorporate new features specific to your use case. You can also look into the customizationService to see +how you can use the registered points to customize the panel. + +
+ +
+ +How to change the order of appearance of panels? + +To change the order of appearance of panels in OHIF v3, you can modify the panel layout configuration in the mode configuration. The panel layout configuration specifies the order and arrangement of panels within the viewer interface. + +
+ +
+ +Is there a way to change the viewer layout to present right panels on the left and the toolbar on the right? + +Not with our default layout which the default extension provides. However, you can write a new layout and provide it +in the `getLayoutModule` which you can reference in the `layout` property of the mode configuration. +
+ +### SopClassHandlerModule + +The least changed module is the SopClassHandlerModule, although this now returns +an array instead of a single instance. The purpose of this module is to create +a list of displaySets based on the metadata. OHIF App uses this module to +create one or more displaySets for each series. +The displaySet is then used to then get assigned +on each viewport and the viewport renders the image. + +The `DisplaySet` created by the handler can have a member function `addInstances` +which will update the display set with new SOP instance data, allowing the +preservation of the display set UID when required. + +Multiple display sets will be returned when different parts of the series are +to be shown separately, for example, to split scout images from volume images. + + +### ViewportModule + +In OHIF v3, viewports are tied to series of SOP Class UIDs (sopClassUIDs). Each extension provides its own viewport for specific SOP Class UIDs, and you can choose which viewports and SOP Class UIDs your mode can handle in the mode configuration. + +For example, in the longitudinal mode configuration, there are multiple viewports specified along with their associated SOP Class Handler Modules: + + +```js +viewports: [ + { + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: [ '@ohif/extension-default.sopClassHandlerModule.stack'], + }, + { + namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr'], + }, + // additional viewports +], +``` + +In this example, there are six viewports specified, each identified by a unique namespace. Each viewport is associated with a specific SOP Class Handler Module through the displaySetsToDisplay property. + +To add a new viewport, you would need to create a new SOP Class Handler Module and a new Viewport Module. The SOP Class Handler Module handles the logic for loading and handling specific SOP Class UIDs, while the Viewport Module defines the rendering and behavior of the viewport. + +In addition to the viewports, the mode configuration should include and register each SOP Class Handler Module that your mode can handle: + + +```js +sopClassHandlers: [ + '@ohif/extension-default.sopClassHandlerModule.stack', + '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', +] +``` + +Here, each SOP Class Handler Module is specified with its namespace. + +By configuring the viewports and SOP Class Handler Modules in your mode, you can define how your mode interacts with different types of DICOM data and specify the appropriate rendering and behavior for each SOP Class UID. + +## Metadata Store and Provider + +In OHIF v2, we utilized the `platform/core/classes/metadata` module, which included the classes StudyeMetadata, SeriesMetadata, and InstanceMetadata for storing metadata. However, in OHIF v3, we have replaced these classes with a more versatile metadata store called `DICOMMetadataStore`. This new metadata store is used by each datasource to store the metadata associated with studies, series, and instances. The DICOMMetadataStore API allows you to add study/series/instance metadata to the store and retrieve metadata from it. + +Although we have transitioned to using DICOMMetadataStore as the primary metadata storage mechanism, you still have access to OHIF's MetadataProvider. The MetadataProvider can be found in the same `platform/core/classes` location. The MetadataProvider is internally used to retrieve instance-based metadata based on UIDs, perform queries, and includes some legacy support for older versions of the loading logic. + + +## Build + +We have recently transitioned from bundling all the extensions and the viewer into a single bundle to a more modular approach. In this new approach, the required extensions are dynamically loaded inside a mode as needed. This change brings several advantages, including: + +- Faster build time: Bundling only the necessary extensions reduces the build time, as you no longer need to bundle all extensions upfront. +- Smaller bundle size: By loading extensions on-demand, the initial bundle size is reduced, resulting in faster page load times for users. +- Faster reload for development: During development, the incremental build process allows for faster reloads, improving developer productivity. + +This new approach does not impact the deployment process of the viewer. You can continue to follow our deployment guides, such as the [Build for Production](../deployment/build-for-production.md) guide, to deploy the viewer effectively. + + +### Script tag usage of the OHIF viewer + +With the transition to more advanced visualization, loading, and rendering techniques using WebWorkers, WASM, and WebGL, the script tag usage of the OHIF viewer has been deprecated. However, if you still prefer to use the script tag usage, it is theoretically possible to bundle all the required dependencies and utilize the script tag approach. + +An alternative option for script tag usage is to employ an `iframe`. You can utilize the iframe element to load the OHIF viewer and establish communication with it using the postMessage API. This allows you to exchange messages and data between the parent window and the iframe, enabling interaction and coordination with the OHIF viewer embedded within the iframe. + +Please note that while these alternatives exist, we recommend utilizing modern development practices and incorporating OHIF viewer within your application using a more modular and integrated approach, such as leveraging bundlers, and import statements to ensure better maintainability, extensibility, and compatibility with the OHIF ecosystem. + + +
+ +I use OHIF v2 in an iframe. Is there any impediment for v3? + +No, there is no impediment for using OHIF v3 in an iframe. OHIF v3 is designed to be compatible with iframe usage, allowing you to embed the viewer within other applications or web pages seamlessly. You can still communicate with the OHIF v3 viewer using the postMessage API to exchange information and trigger actions between the parent window and the embedded iframe. + +
+ + +
+ +Does the build support dynamic imports? How can I use it? + +Yes, the build configuration in OHIF v3 supports dynamic imports. Dynamic imports allow you to asynchronously load modules or components on demand, improving performance and reducing the initial bundle size. In fact we are using this method for our viewport components. In general you can: + +``` +import('path/to/module').then((module) => { + // Use the imported module here +}).catch((error) => { + // Handle any error that occurs during dynamic import +}); +``` + +By using dynamic imports, you can selectively load modules or components at runtime when they are needed, enhancing the efficiency and responsiveness of your application. However, note +that these components must be available at BUILD time, and cannot be updated after +build. + +
+ +
+ + +How can I enhance the existing build to consume my own webpack script? + +You can't enhance the existing build to consume your own webpack script as of now. However, you can +modify the webpack.base.js and webpakc.pwa.js files to add your own webpack script/modules if needed. + +
+ +## UI Components + +Migrating to Tailwind CSS, OHIF v3 is now able to have a component-oriented styling approach, speeding up development, ensuring consistent styling, making responsive design easier, and enabling extensibility + +We have gone through extensive re-design of each part of the UI, and we have also added new components to the OHIF viewer. + +
+ +I have a huge complex styles using native CSS, how can I reuse them? + +You can leverage the power of Tailwind CSS (https://TailwindCSS.com/) in OHIF v3 to reuse your existing styles. Tailwind CSS is a utility-first approach, allowing you to create reusable CSS classes by composing utility classes together. You can migrate your existing styles to Tailwind CSS by breaking them down into utility classes and utilizing the extensive set of predefined utilities provided by Tailwind CSS. + +
+ +
+ +How can I change the page color from being purplish to blueish? + +In OHIF v3, you can easily modify the page color by customizing the Tailwind CSS configuration. You can locate the tailwind.config.js file in your project and update the theme section, specifically the colors property, to define your desired color palette. By adjusting the values for the colors, you can change the page color to any shade of blue or other colors according to your preference. + +
+ +
+ +Can I have my own React UI component working in the application? Is there a way to use the current build for it as well? + +Yes, you can integrate your own React UI components seamlessly into the OHIF v3 application. You can even have external +UI dependencies and by creating your own component inside your extensions and importing it into the application, you can +use it as if it was part of the OHIF v3 application. + +
+ +
+ +How can I replace the existing component ui/tooltip? + +You need to write your own component, and inside your mode layout you can replace the existing component with your own. +As of now, for the tooltip component, you need to use the customizationService to customize it; however, the customizationService +requires a registration of the to-be-customized property before you can customize it. Read more about customizationService. + +
+ +
+ +How can I add/consume logos/images/icons? + + +For logos you can use the whiteLabelling inside the configuration. However, if you need a more complex UI for your toolbar +you need to create you own layout. See `getLayoutModule`. + +
+ +## Redux store + +In OHIF v3, we made the decision to move away from the Redux store and adopt a new approach utilizing React context providers and services with a pub/sub pattern. This shift was driven by the need for a more flexible and scalable architecture that better aligns with the plugin and extension system of OHIF. This offers + +- Modularity and Scalability: Context providers and services enable a modular architecture for easy addition and removal of plugins and extensions. +- Reduced Boilerplate: eliminate Redux boilerplate for simpler development. +- Flexible Pub/Sub Pattern: Services provide a pub/sub pattern for inter-component communication. + +
+ + +Now that redux store is gone, how can I access the user information? + + +You can use the `authenticationService` for that purpose. + +
diff --git a/platform/docs/versioned_docs/version-3.8/platform/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/_category_.json new file mode 100644 index 000000000..842e4abf4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Platform", + "position": 6 +} diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/faq/browser-support.md b/platform/docs/versioned_docs/version-3.8/platform/browser-support.md similarity index 86% rename from platform/docs/versioned_docs/version-2.0-deprecated/faq/browser-support.md rename to platform/docs/versioned_docs/version-3.8/platform/browser-support.md index c4ccb8d2a..fa31439c3 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/faq/browser-support.md +++ b/platform/docs/versioned_docs/version-3.8/platform/browser-support.md @@ -1,10 +1,10 @@ --- -sidebar_position: 3 +sidebar_position: 2 --- # Browser Support The browsers that we support are specified in the `.browserlistrc` file located -in the `platform/viewer` project. While we leverage the latest language features +in the `platform/app` project. While we leverage the latest language features when writing code, we rely on `babel` to _transpile_ our code so that it can run in the browsers that we support. @@ -37,6 +37,7 @@ language feature yet. Our earlier transpilation will rectify _syntax_ discrepancies, but unimplemented features require a "temporary" implementation. That's where polyfills step in. +We previously used polyfill io, but due to a security vulnerability in the library, it's necessary to switch to alternative services. + +## Extension Skeleton + +An extension is a plain JavaScript object that has `id` and `version` properties, and one or +more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks). + +```js +// prettier-ignore +export default { + /** + * Required properties. Should be a unique value across all extensions. + */ + id, + + // Lifecycle + preRegistration() { /* */ }, + onModeEnter() { /* */ }, + onModeExit() { /* */ }, + // Modules + getLayoutTemplateModule() { /* */ }, + getDataSourcesModule() { /* */ }, + getSopClassHandlerModule() { /* */ }, + getPanelModule() { /* */ }, + getViewportModule() { /* */ }, + getCommandsModule() { /* */ }, + getContextModule() { /* */ }, + getToolbarModule() { /* */ }, + getHangingProtocolModule() { /* */ }, + getUtilityModule() { /* */ }, +} +``` + +## OHIF-Maintained Extensions + +A small number of powerful extensions for popular use cases are maintained by +OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in +the top level [`extensions/`][ext-source] directory. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExtensionDescriptionModules
+ + default + + + Default extension provides default viewer layout, a study/series + browser, and a datasource that maps to a DICOMWeb compliant backend + commandsModule, ContextModule, DataSourceModule, HangingProtocolModule, LayoutTemplateModule, PanelModule, SOPClassHandlerModule, ToolbarModule
+ + cornerstone + + + Provides 2d and 3d rendering functionalities + ViewportModule, CommandsModule, UtilityModule
+ dicom-pdf + + Renders PDFs for a specific SopClassUID. + Viewport, SopClassHandler
+ dicom-video + + Renders DICOM Video files. + Viewport, SopClassHandler
+ cornerstone-dicom-sr + + Maintained extensions for cornerstone and visualization of DICOM Structured Reports + ViewportModule, CommandsModule, SOPClassHandlerModule
+ measurement-tracking + + Tracking measurements in the measurement panel + ContextModule,PanelModule,ViewportModule,CommandsModule
+ +## Registering of Extensions + +`viewer` starts by registering all the extensions specified inside the +`pluginConfig.json`, by default we register all extensions in the repo. + + +```js title=platform/app/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.4.0" + }, + { + "packageName": "@ohif/extension-measurement-tracking", + "version": "3.4.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.4.0" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register extensions in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall extensions. Read more [here](../../development/ohif-cli.md) +::: + +The final registration and import of the extensions happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). + +After an extension gets registered within the `viewer`, +each [module](#modules) defined by the extension becomes available to the modes +via the `ExtensionManager` by requesting it via its id. +[Read more about Extension Manager](#extension-manager) + +## Lifecycle Hooks + +Currently, there are three lifecycle hook for extensions: + +[`preRegistration`](./lifecycle/#preRegistration) This hook is called once on +initialization of the entire viewer application, used to initialize the +extensions state, and consume user defined extension configuration. If an +extension defines the [`preRegistration`](./lifecycle/#preRegistration) +lifecycle hook, it is called before any modules are registered in the +`ExtensionManager`. It's most commonly used to wire up extensions to +[services](./../services/index.md) and [commands](./modules/commands.md), and to +bootstrap 3rd party libraries. + +[`onModeEnter`](./lifecycle#onModeEnter): This hook is called whenever a new +mode is entered, or a mode’s data or datasource is switched. This hook can be +used to initialize data. + +[`onModeExit`](./lifecycle#onModeExit): Similarly to onModeEnter, this hook is +called when navigating away from a mode, or before a mode’s data or datasource +is changed. This can be used to cache data for reuse later, but since it +isn't known which mode will be entered next, the state after exiting should be +clean, that is, the same as the state on a clean start. This is called BEFORE +service clean up, and after mode specific onModeExit handling. + +## Modules + +Modules are the meat of extensions, the `blocks` that we have been talking about +a lot. They provide "definitions", components, and filtering/mapping logic that +are then made available to modes and services. + +Each module type has a special purpose, and is consumed by our viewer +differently. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Types + Description
+ + LayoutTemplate + + Control Layout of a route
+ + DataSource + + Control the mapping from DICOM metadata to OHIF-metadata
+ + SOPClassHandler + + Determines how retrieved study data is split into "DisplaySets"
+ + Panel + + Adds left or right hand side panels
+ + Viewport + + Adds a component responsible for rendering a "DisplaySet"
+ + Commands + + Adds named commands, scoped to a context, to the CommandsManager
+ + Toolbar + + Adds buttons or custom components to the toolbar
+ + Context + + Shared state for a workflow or set of extension module definitions
+ + HangingProtocol + + Adds hanging protocol rules
+ + Utility + + Expose utility functions to the outside of extensions
+ +Tbl. Module types +with abridged descriptions and examples. Each module links to a dedicated +documentation page. + +### Contexts + +The `@ohif/app` tracks "active contexts" that extensions can use to scope +their functionality. Some example contexts being: + +- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST` +- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK` + +An extension module can use these to say "Only show this Toolbar Button if the +active viewport is a Cornerstone viewport." This helps us use the appropriate UI +and behaviors depending on the current contexts. + +For example, if we have hotkey that "rotates the active viewport", each Viewport +module that supports this behavior can add a command with the same name, scoped +to the appropriate context. When the `command` is fired, the "active contexts" +are used to determine the appropriate implementation of the rotation behavior. + + + + +[viewers-repo]: https://github.com/OHIF/Viewers +[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions +[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js + diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/installation.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/installation.md new file mode 100644 index 000000000..2e5fb81c2 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/installation.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 5 +sidebar_label: Installation +--- + +# Extension: Installation + +OHIF-v3 provides the ability to utilize external extensions. + + +You can use ohif `cli` tool to install both local and publicly published +extensions on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/lifecycle.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/lifecycle.md new file mode 100644 index 000000000..27868888c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/lifecycle.md @@ -0,0 +1,128 @@ +--- +sidebar_position: 3 +sidebar_label: Lifecycle Hooks +--- + +# Extensions: Lifecycle Hooks + +## Overview + +Extensions can implement specific lifecycle methods. + +- preRegistration +- onModeEnter +- onModeExit + +## preRegistration + +If an extension defines the `preRegistration` lifecycle hook, it is called +before any modules are registered in the `ExtensionManager`. This hook is an +`async` function that can be used to perform: + +- initialize 3rd party libraries +- register event listeners +- add or call services +- add or call commands + +The `preRegistration` hook receives an object containing the +`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any +`configuration` that was provided with the extension at time of registration. + +Example `preRegistration` implementation that register a new service and make it +available in the app. We will talk more in details for creating a new service +for `OHIF-v3`. + +```js +// new service inside new extension +import MyNewService from './MyNewService'; + +export default function MyNewServiceWithServices(servicesManager) { + return { + name: 'MyNewService', + create: ({ configuration = {} }) => { + return new MyNewService(servicesManager); + }, + }; +} +``` + +and + +```js +import MyNewService from './MyNewService' + +export default { + id, + + /** + * @param {object} params + * @param {object} params.configuration + * @param {ServicesManager} params.servicesManager + * @param {CommandsManager} params.commandsManager + * @returns void + */ + async preRegistration({ servicesManager, commandsManager, configuration }) { + console.log('Wiring up important stuff.'); + + window.importantStuff = () => { + console.log(configuration); + }; + + console.log('Important stuff has been wired.'); + window.importantStuff(); + + // Registering new services + servicesManager.registerService(MyNewService(servicesManager)); + }, + }, +}; +``` + +## onModeEnter + +If an extension defines the `onModeEnter` lifecycle hook, it is called when a +new mode is enters, or a mode's data or datasource is switched. + +For instance, in DICOM structured report extension (`dicom-sr`), we are using +`onModeEnter` to re-create the displaySets after a new mode is entered. + +_Example `onModeEnter` hook implementation_ + +```js +export default { + id: '@ohif/extension-cornerstone-dicom-sr', + + onModeEnter({ servicesManager }) { + const { DisplaySetService } = servicesManager.services; + const displaySetCache = DisplaySetService.getDisplaySetCache(); + + const srDisplaySets = displaySetCache.filter( + ds => ds.SOPClassHandlerId === SOPClassHandlerId + ); + + srDisplaySets.forEach(ds => { + // New mode route, allow SRs to be hydrated again + ds.isHydrated = false; + }); + }, +}; +``` + +## onModeExit + +If an extension defines the `onModeExit` lifecycle hook, it is called when +navigating away from a mode. This hook can be used to clean up data tasks such +as unregistering services, removing annotations that do not need to be +persisted. + +_Example `onModeExit` hook implementation_ + +```js +export default { + id: 'myExampleExtension', + + onModeExit({ servicesManager, commandsManager }) { + myCacheService.purge(); + }, +}; +``` diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/extensions/modules/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/_category_.json similarity index 100% rename from platform/docs/versioned_docs/version-2.0-deprecated/extensions/modules/_category_.json rename to platform/docs/versioned_docs/version-3.8/platform/extensions/modules/_category_.json diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/extensions/modules/commands.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/commands.md similarity index 58% rename from platform/docs/versioned_docs/version-2.0-deprecated/extensions/modules/commands.md rename to platform/docs/versioned_docs/version-3.8/platform/extensions/modules/commands.md index dbbbcfdd0..b37202bf6 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/extensions/modules/commands.md +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/commands.md @@ -1,11 +1,14 @@ --- -sidebar_position: 1 +sidebar_position: 2 sidebar_label: Commands --- # Module: Commands ## Overview +`CommandsModule` includes list of arbitrary functions. These may activate tools, communicate with a server, open a modal, etc. +The significant difference between `OHIF-v3` and `OHIF-v2` is that in `v3` a `mode` defines +its toolbar, and which commands each tool call is inside in its toolDefinition An extension can register a Commands Module by defining a `getCommandsModule` method. The Commands Module allows us to register one or more commands scoped to @@ -15,69 +18,55 @@ characteristics that make them tremendously powerful: - Multiple implementations for the same command can be defined - Only the correct command's implementation will be run, dependent on the application's "context" -- Commands can be called from extensions, modules, and the consuming application +- Commands are used by hotkeys, toolbar buttons and render settings Here is a simple example commands module: ```js -export default { - id: 'example-commands-module', - - /** - * @param {object} params - * @param {ServicesManager} params.servicesManager - * @param {CommandsManager} params.commandsManager - */ - getCommandsModule({ servicesManager, commandsManager }) { - return { - definitions: { - sayHello: { - commandFn: ({ words }) => { - console.log(words); - }, - options: { words: 'Hello!' }, - }, +const getCommandsModule = () => ({ + definitions: { + exampleActionDef: { + commandFn: ({ param1 }) => { + console.log(`param1's value is: ${param1}`); }, - defaultContext: 'VIEWER', - }; + // storeContexts: ['viewports'], + options: { param1: 'param1' }, + context: 'VIEWER', // optional + }, }, -}; + defaultContext: 'ACTIVE_VIEWPORT::DICOMSR', +}); ``` + Each definition returned by the Commands Module is registered to the `ExtensionManager`'s `CommandsManager`. +> `storeContexts` has been removed in `OHIF-v3` and now modules have access to all commands and services. This change enables support for user-registered services. + ## Command Definitions -The command definition consists of a named command (`myCommandName` below) and a +The command definition consists of a named command (`exampleActionDef` below) and a `commandFn`. The command name is used to call the command, and the `commandFn` is the "command" that is actioned. ```js -myCommandName: { - commandFn: ({ viewports, other, options }) => { }, - storeContexts: ['viewports'], - options: { words: 'Just kidding! Goodbye!' }, - context: 'ACTIVE_VIEWPORT::CORNERSTONE', +exampleActionDef: { + commandFn: ({ param1, options }) => { }, + options: { param1: 'measurement' }, + context: 'DEFAULT', } ``` | Property | Type | Description | | --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. | -| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instantiation. | | `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` | | `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. | ## Command Behavior -**I have many similar commands. How can I share their `commandFn` and make it -reusable?** -This is where `storeContexts` and `options` come in. We use these in our -`setToolActive` command. `storeContexts` helps us identify our `activeViewport`, -and `options` allow us to pass in the name of a tool we would like to set as -active. **If there are multiple valid commands for the application's active contexts** @@ -91,45 +80,21 @@ active. - When to use: a `hotkey` (like "invert") that doesn't make sense for the current viewport (PDF or HTML) -## `CommandsManager` - -The `CommandsManager` is a class defined in the `@ohif/core` project. A single -instance of it should be defined in the consuming application, and it should be -used when constructing the `ExtensionManager`. - -### Instantiating - -When we instantiate the `CommandsManager`, we need to pass it two methods: - -- `getAppState` - Should return the application's state when called -- `getActiveContexts` - Should return the application's active contexts when - called - -These methods are used internally to help determine which commands are currently -valid, and how to provide them with any state they may need at the time they are -called. - -```js -const commandsManager = new CommandsManager({ - getAppState, - getActiveContexts, -}); -``` - -### Public API +## `CommandsManager` Public API If you would like to run a command in the consuming app or an extension, you can -use one of the following methods: +use `CommandsManager.runCommand(commandName, options = {}, contextName)` + ```js // Returns all commands for a given context commandsManager.getContext('string'); -// Attempts to run a command +// Run a command, it will run all the `speak` commands in all contexts commandsManager.runCommand('speak', { command: 'hello' }); -// Run command, but override the active contexts -commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']); +// Run command, from Default context +commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']); ``` The `ExtensionManager` handles registering commands and creating contexts, so @@ -150,7 +115,7 @@ It is up to the consuming application to define what contexts are possible, and which ones are currently active. As extensions depend heavily on these, we will likely publish guidance around creating contexts, and ways to override extension defined contexts in the near future. If you would like to discuss potential -changes to how contexts work, please don't hesitate to create new GitHub +changes to how contexts work, please don't hesitate to create a new GitHub issue. [Some additional information on Contexts can be found here.](./../index.md#contexts) diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/contextModule.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/contextModule.md new file mode 100644 index 000000000..35de381a3 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/contextModule.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 9 +sidebar_label: Context +--- +# Module: Context + +## Overview +This new module type allows you to connect components via a shared context. You can create a context that two components, e.g. a viewport and a panel can use to synchronize and communicate. An extensive example of this can be seen in the longitudinal mode’s custom extensions. + + + +```jsx +const ExampleContext = React.createContext(); + +function ExampleContextProvider({ children }) { + return ( + + {children} + + ); +} + +const getContextModule = () => [ + { + name: 'ExampleContext', + context: ExampleContext, + provider: ExampleContextProvider, + }, +]; +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/data-source.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/data-source.md new file mode 100644 index 000000000..7b7a2d506 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/data-source.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 3 +sidebar_label: Data Source +--- + +# Module: Data Source + +## Overview + +We have built couple of methods for fetching and mapping data into OHIF’s native +format, which we call DataSources, and have provided one implementation of this +standard. + +You can make another datasource implementation which communicates to your +backend and maps to OHIF’s native format, then use any existing mode on your +platform. Your data doesn’t even need to be DICOM if you can map some +proprietary data to the correct format. + +The DataSource is also a place to add easy helper methods that platform-specific +extensions can call in order to interact with the backend, meaning proprietary +data interactions can be wrapped in extensions. + +```js +const getDataSourcesModule = () => [ + { + name: 'exampleDataSource', + type: 'webApi', // 'webApi' | 'local' | 'other' + createDataSource: dataSourceConfig => { + return IWebApiDataSource.create(/* */); + }, + }, +]; +``` + +Default extension provides two main data sources that are commonly used: +`dicomweb` and `dicomjson` + +```js +import { createDicomWebApi } from './DicomWebDataSource/index.js'; +import { createDicomJSONApi } from './DicomJSONDataSource/index.js'; + +function getDataSourcesModule() { + return [ + { + name: 'dicomweb', + type: 'webApi', + createDataSource: createDicomWebApi, + }, + { + name: 'dicomjson', + type: 'jsonApi', + createDataSource: createDicomJSONApi, + }, + ]; +} +``` + +## Custom DataSource + +You can add your custom datasource by creating the implementation using +`IWebApiDataSource.create` from `@ohif/core`. This factory function creates a +new "Web API" data source that fetches data over HTTP. + +```js title="platform/core/src/DataSources/IWebApiDataSource.js" +function create({ + initialize, + query, + retrieve, + store, + reject, + parseRouteParams, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet, + getImageIdsForInstance, +}) { + /* */ +} +``` + +You can take a look at `dicomweb` data source implementation to get an idea +`extensions/default/src/DicomWebDataSource/index.js` but here here are some +important api endpoints that you need to implement: + +- `initialize`: This method is called when the data source is first created in the mode.tsx, it is used to initialize the data source and set the configuration. For instance, `dicomwebDatasource` uses this method to grab the StudyInstanceUID from the URL and set it as the active study, as opposed to `dicomJSONDatasource` which uses url in the browser to fetch the data and store it in a cache +- `query.studies.search`: This is used in the study panel on the left to fetch the prior studies for the same MRN which is then used to display on the `All` tab. it is also used in the Worklist to show all the studies from the server. +- `query.series.search`: This is used to fetch the series information for a given study that is expanded in the Worklist. +- `retrieve.bulkDataURI`: used to render RTSTUCTURESET in the viewport. +- `retrieve.series.metadata`: It is a crucial end point that is used to fetch series level metadata which for hanging displaySets and displaySet creation. +- `store.dicom`: If you don't need store functionality, you can skip this method. This is used to store the data in the backend. + +## Static WADO Client + +If the configuration for the data source has the value staticWado set, then it +is assumed that queries for the studies return a super-set of the studies, as it +is assumed to be returning a static list. The StaticWadoClient performs the +search functionality manually, by interpreting the query parameters and then +applying them to the returned response. This functionality may be useful for +other types of DICOMweb back ends, where they are capable of performing queries, +but don't allow for querying certain types of fields. However, that only works +as long as the size of the studies list isn't too large that client side +selection isn't too expensive. + +## DicomMetadataStore + +In `OHIF-v3` we have a central location for the metadata of studies, and they are +located in `DicomMetadataStore`. Your custom datasource can communicate with +`DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will +learn more about `DicomMetadataStore` in services. + +## Adding a Data Source Outside a Module + +A data source can be added outside a module via `ExtensionManager.addDataSource`. +The following snippet of code demonstrates how `addDataSource` can be used to add +a new DICOMWeb data source for the Google Cloud Healthcare API and set it as the +active data source. + +```js +extensionManager.addDataSource({ + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'google', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, + {activate:true} +}); +``` + +## Updating a Data Source's Configuration + +An existing data source can have its configuration updated using the +`ExtensionManager.updateDataSourceConfiguration` method. The following snippet of +code demonstrates how `updateDataSourceConfiguration` can be use to update the +configuration of an existing DICOMWeb data source (named `dicomweb`) with the +configuration for a Google Cloud Healthcare API data source. + +```js +extensionManager.updateDataSourceConfiguration( "dicomweb", + { + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, +); +``` + +## Merge Data Source +The built-in merge data source is a useful tool for combining results from multiple data sources. +Currently, this data source only supports merging at the series level. This means that series from data source 'A' +and series from data source 'B' will be retrieved under the same study. If the same series exists in both data sources, +the first series arrived is the one that gets stored, and any other conflicting series will be ignored. + +The merge data source is particularly useful when dealing with derived data that is generated and stored in different servers. +For example, it can be used to retrieve annotation series from one data source and input data (images) from another data source. + +A default data source can be defined as shown below. This allows defining which of the servers should be the +fallback server in case something goes wrong. + +Configuration Example: +```js +window.config = { + ... + dataSources: [ + { + sourceName: 'merge', + namespace: '@ohif/extension-default.dataSourcesModule.merge', + configuration: { + name: 'merge', + friendlyName: 'Merge dicomweb-1 and dicomweb-2 data at the series level', + seriesMerge: { + dataSourceNames: ['dicomweb-1', 'dicomweb-2'], + defaultDataSourceName: 'dicomweb-1' + }, + }, + }, + { + sourceName: 'dicomweb-1', + ... + }, + { + sourceName: 'dicomweb-2', + ... + }, + ], +}; +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/hpModule.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/hpModule.md new file mode 100644 index 000000000..e29f97609 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/hpModule.md @@ -0,0 +1,624 @@ +--- +sidebar_position: 8 +sidebar_label: Hanging Protocol +--- +# Module: Hanging Protocol + +## Overview + +[Hanging protocols](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf) are an essential part of any radiology viewer. +OHIF uses Hanging Protocols to handle the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The +winning protocol (highest score) gets applied and its settings run for the viewports +to be arranged. + + +In `OHIF-v3` hanging protocols you can: + +- Define what layout the viewport should starts with (e.g., 2x2 layout) +- Specify the type of the viewport and its orientation (e.g., stack, volume with Sagittal view) +- Define which displaySets gets displayed in which viewport of the layout (e.g,. displaySet that has modality of 'CT' and 'SeriesDescription' of 'Coronary Arteries' gets displayed in the first viewport of the layout) +- Apply certain initial viewport settings (e.g., inverting the contrast, jumping to a specific slice, etc.) +- Add specific synchronization rules for the viewports (e.g., synchronize the zoom of the viewports of the index 1, 2 OR synchronize the VOI of the viewports of the index 2, 3) + + +Using `hangingProtocolModule` you can provide/register the protocols for OHIF to +utilize. + +Here is an example protocol which if used will hang a 1x3 layout with the first viewport showing a CT image, the second viewport showing a PT image and the third viewport showing their fusion, all in Sagittal orientations to achieve a view of + + +![](../../../assets/img/hangingProtocolExample.png) + + +```js +const oneByThreeProtocol = { + id: 'oneByThreeProtocol', + locked: true, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2022-10-04T19:22:08.894Z', + availableTo: {}, + editableBy: {}, + imageLoadStrategy: 'interleaveTopToBottom', + protocolMatchingRules: [ + { + attribute: 'ModalitiesInStudy', + constraint: { + contains: ['CT', 'PT'], + }, + }, + ], + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + ], + }, + }, + stages: [ + { + id: 'hYbmMy3b7pz7GLiaT', + name: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + }, + displaySets: [ + { + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { + colormap: 'hsv', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + }, + id: 'ptDisplaySet', + }, + ], + }, + ], + createdDate: '2021-02-23T18:32:42.850Z', + }, + ], + numberOfPriorsReferenced: -1, +}; + + +function getHangingProtocolModule() { + return [ + { + id: 'oneByThreeProtocol', + protocol: oneByThreeProtocol, + }, + ]; +} +``` + + +## Skeleton of a Protocol + +The skeleton of a hanging protocol is as follows: + + +### Id +unique identifier for the protocol, this id can be used inside mode configuration +to specify which protocol should be used for a specific mode. A mode can +request a protocol by its id (which makes OHIF to apply the protocol without +matching), or provides an array of ids which will +make the ProtocolEngine to choose the best matching protocol (based on +protocolMatching rules, which is next section). + +### imageLoadStrategy +The image load strategy specifies a function (by name) containing logic to re-order +the image load requests. This allows loading images viewed earlier to be done +sooner than those loaded later. The available strategies are: + +* interleaveTopToBottom to start at the top and work towards the bottom, for all series being loaded +* interleaveCenter is like top to bottom but starts at the center +* nth is a strategy that loads every nth instance, starting with the center +and end points, and then filling in progressively all along the image. This results in partial +image view very quickly. + +### protocolMatchingRules +A list of criteria for the protocol along with the provided points for ranking. + + - `weight`: weight for the matching rule. Eventually, all the registered + protocols get sorted based on the weights, and the winning protocol gets + applied to the viewer. + - `attribute`: tag that needs to be matched against. This can be either + Study-level metadata or a custom attribute such as "StudyInstanceUID", + "StudyDescription", "ModalitiesInStudy", "NumberOfStudyRelatedSeries", "NumberOfSeriesRelatedInstances" + In addition to these tags, you can also use a custom attribute that you have registered before. + We will learn more about this later. + - `from`: Indicates the source of the attribute. This allows getting values + from other objects such as the `prior` instance object instead of from the + current one. + + + + - `constraint`: the constraint that needs to be satisfied for the attribute. It accepts a `validator` which can be + [`equals`, `doesNotEqual`, `contains`, `doesNotContain`, `startsWith`, `endsWidth`] + + | Rule | Single Value | Array Value | Example | +|------|--------------|-------------|---------| +| equals | === | All members are === in same order | value = ['abc', 'def', 'GHI']
testValue = 'abc' (Fail)
= ['abc'] (Fail)
= ['abc', 'def', 'GHI'] (Valid)
= ['abc', 'GHI', 'def'] (Fail)
= ['abc', 'def'] (Fail)

value = 'Attenuation Corrected'
testValue = 'Attenuation Corrected' (Valid)
= 'Attenuation' (Fail)

value = ['Attenuation Corrected']
testValue = ['Attenuation Corrected'] (Valid)
= 'Attenuation Corrected' (Valid)
= 'Attenuation' (Fail) | +| doesNotEqual | !== | Any member is !== for the array, either in value, order, or length | value = ['abc', 'def', 'GHI']
testValue = 'abc' (Valid)
= ['abc'] (Valid)
= ['abc', 'def', 'GHI'] (Fail)
= ['abc', 'GHI', 'def'] (Valid)
= ['abc', 'def'] (Valid)

value = 'Attenuation Corrected'
testValue = 'Attenuation Corrected' (Fail)
= 'Attenuation' (Valid)

value = ['Attenuation Corrected']
testValue = ['Attenuation Corrected'] (Fail)
= 'Attenuation Corrected' (Fail)
= 'Attenuation' (Fail) | +| includes | Not allowed | Value is equal to one of the values of the array | value = ['abc', 'def', 'GHI']
testValue = ['abc'] (Valid)
= 'abc' (Fail)
= ['abc'] (Fail)
= 'dog' (Fail)
= ['att', 'abc'] (Valid)
= ['abc', 'def', 'dog'] (Valid)
= ['cat', 'dog'] (Fail)

value = 'Attenuation Corrected'
testValue = ['Attenuation Corrected', 'Corrected'] (Valid)
= ['Attenuation', 'Corrected'] (Fail)

value = ['Attenuation Corrected']
testValue = 'Attenuation Corrected' (Fail)
= ['Attenuation Corrected', 'Corrected'] (Valid)
= ['Attenuation', 'Corrected'] (Fail) | +| doesNotInclude | Not allowed | Value is not in one of the values of the array | value = ['abc', 'def', 'GHI']
testValue = 'Corr' (Valid)
= 'abc' (Fail)
= ['att', 'cor'] (Valid)
= ['abc', 'def', 'dog'] (Fail)

value = 'Attenuation Corrected'
testValue = ['Attenuation Corrected', 'Corrected'] (Fail)
= ['Attenuation', 'Corrected'] (Valid)

value = ['Attenuation Corrected']
testValue = 'Attenuation' (Fail)
= ['Attenuation Corrected', 'Corrected'] (Fail)
= ['Attenuation', 'Corrected'] (Valid) | +| containsI | String containment (case insensitive) | String containment (case insensitive) is OK for one of the rule values | value = 'Attenuation Corrected'
testValue = 'Corr' (Valid)
= 'corr' (Valid)
= ['att', 'cor'] (Valid)
= ['Att', 'Wall'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Valid)
= 'dog' (Fail)
= ['gh', 'de'] (Valid)
= ['cat', 'dog'] (Fail) | +| contains | String containment (case sensitive) | String containment (case sensitive) is OK for one of the rule values | value = 'Attenuation Corrected'
testValue = 'Corr' (Valid)
= 'corr' (Fail)
= ['att', 'cor'] (Fail)
= ['Att', 'Wall'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Valid)
= 'dog' (Fail)
= ['cat', 'de'] (Valid)
= ['cat', 'dog'] (Fail) | +| doesNotContain | String containment is false | String containment is false for all values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'corr' (Valid)
= ['att', 'cor'] (Valid)
= ['Att', 'Wall'] (Fail)
= ['cat', 'dog'] (Valid)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Fail)
= 'dog' (Valid)
= ['cat', 'de'] (Fail)
= ['cat', 'dog'] (Valid) | +| doesNotContainI | String containment is false (case insensitive) | String containment (case insensitive) is false for all values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'corr' (Fail)
= ['att', 'cor'] (Fail)
= ['Att', 'Wall'] (Fail)
= ['cat', 'dog'] (Valid)

value = ['abc', 'def', 'GHI']
testValue = 'DEF' (Fail)
= 'dog' (Valid)
= ['cat', 'gh'] (Fail)
= ['cat', 'dog'] (Valid) | +| startsWith | Value begins with characters | Starts with one of the values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'Att' (Fail)
= ['cat', 'dog', 'Att'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'deg' (Valid)
= ['cat', 'GH'] (Valid)
= ['cat', 'gh'] (Fail)
= ['cat', 'dog'] (Fail) | +| endsWith | Value ends with characters | ends with one of the value of the array | value = 'Attenuation Corrected'
testValue = 'TED' (Fail)
= 'ted' (Valid)
= ['cat', 'dog', 'ted'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'deg' (Valid)
= ['cat', 'HI'] (Valid)
= ['cat', 'hi'] (Fail)
= ['cat', 'dog'] (Fail) | +| greaterThan | value is >= to rule | Not applicable | value = 30
testValue = 20 (Valid)
= 40 (Fail) | +| lessThan | value is <= to rule | Not applicable | value = 30
testValue = 40 (Valid)
= 20 (Fail) | +| range | Not applicable | 2 value requested (min and max) | value = 50
testValue = [10,60] (Valid)
= [60, 10] (Valid)
= [0, 10] (Fail)
= [70, 80] (Fail)
= 45 (Fail)
= [45] (Fail) | +| notNull | Not Applicable | Not Applicable | No value | | + A sample of the matching rule is above which matches against the study description to be PETCT + + ```js + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PETCT', + }, + }, + required: false, + }, + ``` + +### `from` attribute (optional) +The `from` attribute allows you to retrieve the attribute to test from another object, such as the previous study, the overall list of studies, or another provided value from a module. + +The values provided by OHIF which you can use are: + +- `activeStudy`: to use the metadata of the active study to match +- `studies`: to use the metadata of the list of studies (all studies) to match +- `allDisplaySets`: all available display sets +- `displaySets`: if the selector has matched a study, these are the display sets for that study +- `prior`: the metadata of the first study in the list of studies that is not the active study +- `options`: during matching, we also provide an options object with the following information that you can use as the `from` value: + - `studyInstanceUIDsIndex`: the index of the study in the list of studies + - `instance`: the metadata of the instance being matched, which is exactly the displaySet.instance metadata. + + +### displaySetSelectors (mandatory) +Defines the display sets that the protocol will use for arrangement. + +```js + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + ], + }, + } +``` + +As you see above we have specified two displaysets: 1) ctDisplaySet , 2) ptDisplaySet +The ctDisplaySet will match against all the series that are CT and reconstructable +The ptDisplaySet will match against all the series that are PT and reconstructable. + +As you see each selector is composed of an `id` as the key and a set of `seriesMatchingRules` (displaySetMatchingRules) which gives score to the displaySet +based on the matching rules. The displaySet with the highest score will be used for the `id`. + +### stages +Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules. Therefore, the `stages` property is an array of objects, each object being one stage. + +### viewportStructure +Defines the layout of the viewer. You can define the number of `rows` and `columns`. There should be `rows * columns` number of +viewport configuration in the `viewports` property. Note that order of viewports are rows first then columns. + +```js +viewportStructure: { + type: 'grid', + properties: { + rows: 1, + columns: 2, + viewportOptions: [], + }, +}, +``` + +In addition to the equal viewport sizes, you can define viewports to span multiple rows or columns. + +```js +viewportStructure: { + type: 'grid', + properties: { + rows: 1, + columns: 2, + viewportOptions: [ + { + x: 0, + y: 0, + width: 1 / 4, + height: 1, + }, + { + x: 1 / 4, + y: 0, + width: 3 / 4, + height: 1, + }, + ], + }, +}, + +``` + + +### viewports +This field includes the viewports that will get hung on the viewer. + +```js +viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + // the rest +], +``` + +As you can see in the hanging protocol we defined three viewports (but only showing one of them right above). Each viewport has two properties: + +1. `viewportOptions`: defines the viewport properties such as + - `viewportId`: unique identifier for the viewport (optional) + - `viewportType`: type of the viewport (optional - options: stack, volume - default is stack + - `background`: background color of the viewport (optional) + - `orientation`: orientation of the viewport (optional - if not defined for volume -> acquisition axis) + - `toolGroupId`: tool group that will be used for the viewport (optional) + - `initialImageOptions`: initial image options (optional - can be specific imageIndex number or preset (first, middle, last)) + - `syncGroups`: sync groups for the viewport (optional) + -The `displayArea` parameter refers to the designated area within the viewport where a specific portion of the image can be displayed. This parameter is optional and allows you to choose the location of the image within the viewport. For example, in mammography images, you can display the left breast on the left side of the viewport and the right breast on the right side, with the chest wall positioned in the middle. To understand how to define the display area, you can refer to the live example provided by CornerstoneJS [here](https://www.cornerstonejs.org/live-examples/programaticpanzoom). + + +2. `displaySets`: defines the display sets that are displayed on a viewport. It is an array of objects, each object being one display set. + - `id`: id of the display set (required) + - `options` (optional): options for the display set + - voi: windowing options for the display set (optional: windowWidth, windowCenter) + - voiInverted: whether the VOI is inverted or not (optional) + - colormap: colormap for the display set (optional, it is an object with `{ name }` and optional extra `opacity` property) + - displayPreset: display preset for the display set (optional, used for 3D volume rendering. e.g., 'CT-Bone') + + +### Custom attribute +For any matching rules you can specify a custom attribute too. For instance, +if you have a timepoint attribute in for each of your studies, you can use that in the matching rules. + +```js +{ + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'timepoint', + constraint: { + equals: { + value: 'baseline', + }, + }, + required: false, +}, +``` + +and then you need to register a callback in the HangingProtocolService to get the value for the attribute. + +```js +HangingProtocolService.addCustomAttribute( + 'timepoint', // attributeId + 'addCustomAttribute', // attributeName + study => { // callback that returns the value for the attribute + const timePoint = fetchFromMyCustomBackend(study.studyInstanceUid); + return timePoint; + } +); +``` + + +## Matching on Prior Study with UID + +Often it is desired to match a new study to a prior study (e.g., follow up on +a surgery). Since the hanging protocols run on displaySets we need to have a +way to let OHIF knows that it needs to load the prior study as well. This can +be done by specifying both StudyInstanceUIDs in the URL. The additional studies +are then accessible to the hanging protocol. Below we are +running OHIF with two studies, and a comparison hanging protocol available by +default. + +```bash +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&hangingprotocolId=@ohif/hpCompare +``` + +The `&hangingProtocolId` option forces the specific hanging protocol to be +applied, but the mode can also add the hanging protocols to the default set, +and then the best matching hanging protocol will be applied by the run method. + +To match any other studies, it is required to enable the prior matching rules +capability using: + +```javascript + // Indicate number of priors used - 0 means any number, -1 means none. + numberOfPriorsReferenced: 1, +``` + +The matching rule that allows the hanging protocol to be runnable is: + +```javascript + protocolMatchingRules: [ + { + id: 'Two Studies', + weight: 1000, + // This will generate 1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1 + // since that is study instance UID in the prior from instance. + attribute: 'StudyInstanceUID', + // The 'from' attribute says where to get the 'attribute' value from. In this case + // prior means the second study in the study list. + from: 'prior', + required: true, + constraint: { + notNull: true, + }, + }, + ], +``` + +The display set selector selecting the specific study to display is included +in the studyMatchingRules. Note that this rule will cause ONLY the second study +to be matched, so it won't attempt to match anything in other studies. +Additional series level criteria, such as modality rules must be included at the +`seriesMatchingRules`. + +```javascript + studyMatchingRules: [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 1 }, + }, + }, + ], +``` + + +## Callbacks + + +Hanging protocols in `OHIF-v3` provide the flexibility to define various callbacks that allow you to customize the behavior of your viewer when specific events occur during protocol execution. These callbacks are defined in the `ProtocolNotifications` type and can be added to your hanging protocol configuration. + +Each callback is an array of commands or actions that are executed when the event occurs. + +```js +[ + { + commandName: 'showDownloadViewportModal', + commandOptions: {} + } +] +``` + + +Here, we'll explain the available callbacks and their purposes: + +### `onProtocolExit` + +The `onProtocolExit` callback is executed after the protocol is exited and the new one is applied. This callback is useful for performing actions or executing commands when switching between hanging protocols. + +### `onProtocolEnter` + +The `onProtocolEnter` callback is executed after the protocol is entered and applied. You can use this callback to define actions or commands that should run when entering a specific hanging protocol. + +### `onLayoutChange` + +The `onLayoutChange` callback is executed before the layout change is started. You can use it to apply a specific hanging protocol based on the current layout or other criteria. + +### `onViewportDataInitialized` + +The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start. + +Here is an example of how you can add these callbacks to your hanging protocol configuration: + +```javascript +const protocol = { + id: 'myProtocol', + name: 'My Protocol', + // rest of the protocol configuration + callbacks: { + onProtocolExit: [ + // Array of commands or actions to execute on protocol exit + ], + onProtocolEnter: [ + // Array of commands or actions to execute on protocol enter + ], + onLayoutChange: [ + // Array of commands or actions to execute on layout change + ], + onViewportDataInitialized: [ + // Array of commands or actions to execute on viewport data initialization + ], + }, + // protocolMatchingRules + // the rest +}; diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/layout-template.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/layout-template.md new file mode 100644 index 000000000..821444975 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/layout-template.md @@ -0,0 +1,139 @@ +--- +sidebar_position: 7 +sidebar_label: Layout Template +--- + +# Module: Layout Template + +## Overview + +`LayoutTemplates` are a new concept in v3 that modes use to control the layout +of a route. A layout template is a React component that is given a set of +managers that define apis to access toolbar state, commands, and hotkeys, as +well as props defined by the layout template. + +For instance the default LayoutTemplate takes in leftPanels, rightPanels and +viewports as props, which it uses to build its view. + +In addition, `layout template` has complete control over the structure of the +application. You could have tools down the left side, or a strict guided +workflow with tools set programmatically, the choice is yours for your use case. + +```jsx +const getLayoutTemplateModule = (/* ... */) => [ + { + id: 'exampleLayout', + name: 'exampleLayout', + component: ExampleLayoutComponent, + }, +]; +``` + +The `props` that are passed to `layoutTemplate` are managers and service, along +with the defined mode left/right panels, mode's defined viewports and OHIF +`ViewportGridComp`. LayoutTemplate leverages extensionManager to grab typed +extension module entries: `*.getModuleEntry(id)` + +A simplified code for `Default extension`'s layout template is: + +```jsx title="extensions/default/src/ViewerLayout/index.jsx" +import React from 'react'; +import { SidePanel } from '@ohif/ui'; + +function Toolbar({ servicesManager }) { + const { ToolBarService } = servicesManager.services; + + return ( + <> + // ToolBarService.getButtonSection('primary') to get toolbarButtons + {toolbarButtons.map((toolDef, index) => { + const { id, Component, componentProps } = toolDef; + return ( + ToolBarService.recordInteraction(args)} + /> + ); + })} + + ); +} + +function ViewerLayout({ + // From Extension Module Params + extensionManager, + servicesManager, + hotkeysManager, + commandsManager, + // From Modes + leftPanels, + rightPanels, + viewports, + ViewportGridComp, +}) { + const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; + }; + + const getViewportComponentData = viewportComponent => { + const entry = extensionManager.getModuleEntry(viewportComponent.namespace); + + return { + component: entry.component, + displaySetsToDisplay: viewportComponent.displaySetsToDisplay, + }; + }; + + const leftPanelComponents = leftPanels.map(getPanelData); + const rightPanelComponents = rightPanels.map(getPanelData); + const viewportComponents = viewports.map(getViewportComponentData); + + return ( +
+ + +
+ {/* LEFT SIDEPANELS */} + + + {/* TOOLBAR + GRID */} + + + {/* Right SIDEPANELS */} + +
+
+ ); +} +``` + +## Overview Video + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/panel.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/panel.md new file mode 100644 index 000000000..9a9235cee --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/panel.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 6 +sidebar_label: Panel +--- + +# Module: Panel + +## Overview + +The default LayoutTemplate has panels on the left and right sides, however one +could make a template with panels at the top or bottom and make extensions with +panels intended for such slots. + +An extension can register a Panel Module by defining a `getPanelModule` method. +The panel module provides the ability to define `menuOptions` and `components` +that can be used by the consuming application. `components` are React Components +that can be displayed in the consuming application's "Panel" Component. + +![panel-module-v3](../../../assets/img/panel-module-v3.png) + +The `menuOptions`'s `target` key, points to a registered `components`'s `id`. A +`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can +optionally provide its own `context` value. + +The `getPanelModule` receives an object containing the `ExtensionManager`'s +associated `ServicesManager` and `CommandsManager`. + +An extension can also trigger to activate/open a panel via the `PanelService` - +either by explicitly calling `PanelService.activatePanel` or triggering panel +activation when some other event fires. + +```jsx +import PanelMeasurementTable from './PanelMeasurementTable.js'; + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedMeasurementPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'measure', + iconName: 'list-bullets', + iconLabel: 'Measure', + label: 'Measurements', + isDisabled: studies => {}, // optional + component: wrappedMeasurementPanel, + }, + ]; +} +``` + +## Consuming Panels Inside Modes + +As explained earlier, extensions make the functionalities and components +available and `modes` utilize them to build an app. So, as seen above, we are +not actually defining which side the panel should be opened. Our extension is +providing the component with its. + +New: You can easily add multiple panels to the left/right side of the viewer +using the mode configuration. As seen below, the `leftPanels` and `rightPanels` +accept an `Array` of the `IDs`. The mode configuration also allows for either (or +both) side panels to be closed by default. In the code below, the right panel +is closed by default. The mode can optionally add event triggers to +the `PanelService` that when fired will cause a side panel that was defaulted +closed to open. In the code below, the right side panel, that contains the +`trackedMeasurements` panel, is triggered to open when a measurement is added. +Note that once a default closed side panel has been opened once, +only a `PanelService.EVENTS.ACTIVATE_PANEL` event with `forceActive === true` +will cause it open (again). + +```js + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', +}; + +const id = 'viewer' +const version = '3.0.0 + +function modeFactory({ modeConfiguration }) { + let _activatePanelTriggersSubscriptions = []; + return { + id, + routes: [ + { + path: 'longitudinal', + layoutTemplate: ({ location, servicesManager }) => { + return { + id, + props: { + leftPanels: [ + '@ohif/extension-measurement-tracking.panelModule.seriesList', + ], + rightPanels: [ + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + ], + rightPanelClosed: true, + viewports, + }, + }; + }, + }, + ], + onModeEnter: ({ servicesManager }) => { + const { + measurementService, + panelService, + } = servicesManager.services; + + _activatePanelTriggersSubscriptions = [ + ...panelService.addActivatePanelTriggers('@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', [ + { + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + }, + ]), + ]; + }, + onModeExit: () => { + _activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe()); + _activatePanelTriggersSubscriptions = []; + }, + extensions: extensionDependencies + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +``` + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/sop-class-handler.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/sop-class-handler.md new file mode 100644 index 000000000..cf70fb42f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/sop-class-handler.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 4 +sidebar_label: SOP Class Handler +--- +# Module: SOP Class Handler + +## Overview +This module defines how a specific DICOM SOP class should be processed to make a list of `DisplaySet`, things which can be hung in a viewport. An extension can register a [SOP Class][sop-class-link] Handler Module by defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]. + +The mode chooses what SOPClassHandlers to use, so you could process a series in a different way depending on mode within the same application. + +SOPClassHandler is a bit different from the other modules, as it doesn't provide a `1:1` +schema for UI or provide its own components. It instead defines: + +- `sopClassUIDs`: an array of string SOP Class UIDs that the + `getDisplaySetFromSeries` method should be applied to. +- `getDisplaySetFromSeries`: a method that maps series and study metadata to a + display set + +A `displaySet` has the following shape: + +```js +return { + Modality: 'MR', + displaySetInstanceUIDD + SeriesDate, + SeriesTime, + SeriesInstanceUID, + StudyInstanceUID, + SeriesNumber, + FrameRate, + SeriesDescription, + isMultiFrame, + numImageFrames, + SOPClassHandlerId, + madeInClient, +} +``` + +## Example SOP Class Handler Module + +```js +import ImageSet from '@ohif/core/src/classes/ImageSet'; + + +const sopClassDictionary = { + CTImageStorage: "1.2.840.10008.5.1.4.1.1.2", + MRImageStorage: "1.2.840.10008.5.1.4.1.1.4", +}; + + +// It is important to note that the used SOPClassUIDs in the modes are in the order that is specified in the array. +const sopClassUids = [ + sopClassDictionary.CTImageStorage, + sopClassDictionary.MRImageStorage, +]; + +function addInstances(instances) { + // Add instances to this display set, and return the display set updated. +} + +const makeDisplaySet = (instances) => { + const instance = instances[0]; + const imageSet = new ImageSet(instances); + + imageSet.setAttributes({ + displaySetInstanceUID: imageSet.uid, + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesNumber: instance.SeriesNumber, + FrameRate: instance.FrameTime, + SeriesDescription: instance.SeriesDescription, + Modality: instance.Modality, + isMultiFrame: isMultiFrame(instance), + numImageFrames: instances.length, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, + addInstances, + }); + + // Note returns an array now + return [imageSet]; +}; + +getSopClassHandlerModule = () => { + return [ + { + name: 'stack, + sopClassUids, + getDisplaySetsFromSeries: makeDisplaySet, + }, + ]; +}; + +``` + +### addInstances +In order to allow new SOP instances to be received and added to an existing display +set, the addInstances method can be added to a display set. It is called +on the display set to be updated, and returns it when it has added at least one +of the instances to the display set. + +### More examples : +You can find another example for this mapping between raw metadata and displaySet for +`DICOM-SR` extension. + +## `@ohif/app` usage + +We use the `sopClassHandlerModule`s in `DisplaySetService` where we +transform instances from the raw metadata format to a OHIF displaySet format. +You can read more about DisplaySetService here. + + +[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html +[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12 +[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6 +[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7 +[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7 + diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/toolbar.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/toolbar.md new file mode 100644 index 000000000..94fdb22fc --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/toolbar.md @@ -0,0 +1,522 @@ +--- +sidebar_position: 1 +sidebar_label: Toolbar +--- + +# Module: Toolbar + +An extension can register a Toolbar Module by defining a `getToolbarModule` +method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides the +following toolbar button `uiTypes`: + +- `ohif.radioGroup`: which is a simple button that can be clicked +- `ohif.splitButton`: which is a button with a dropdown menu +- `ohif.divider`: which is a simple divider + +## Example Toolbar Module + +The Toolbar Module should return an array of `objects`. There are currently a +few different variations of definitions, each one is detailed further down. +There are two things that the toolbar module can provide, first +a component, and second evaluators. + +### Components +```js +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, + { + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, + }, + { + name: 'ohif.layoutSelector', + defaultComponent: ToolbarLayoutSelector, + clickHandler: (evt, clickedBtn, btnSectionName) => {}, + }, + { + name: 'ohif.toggle', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, + ]; +} +``` + +### Custom Components + +You can also create your own extension, and add your new custom tool appearance +(e.g., split horizontally instead of vertically for split tool). Simply add +`getToolbarModule` to your extension, and pass your tool react component to its +`defaultComponent` property in the returned object. You can use `@ohif/ui` +components such as `IconButton, Icon, Tooltip, ToolbarButton` to build your own +component. + +```js +import myToolComponent from './myToolComponent'; + +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'new-tool-type', + defaultComponent: myToolComponent, + clickHandler: () => {}, + }, + ]; +} +``` + +Check out how to assemble the toolbar in the [modes](../../modes/index.md) section. + + +### Evaluators +Buttons may be equipped with evaluators, which are functions invoked by the toolbarService to assess the button's status. These evaluators are expected to return an object of `{className}` and may include additional details, as elaborated in the subsequent section. + +Evaluators play a crucial role in determining the button's status based on the viewport. For example, users should be restricted from clicking on the mpr if the displaySet is not reconstructable. Additionally, certain buttons within the toolbar may be associated with specific toolGroups and should remain inactive for certain viewports. + +Let's look at one of the evaluators (for `evaluate.cornerstoneTool`) + +```js + { + name: 'evaluate.cornerstoneTool', + evaluate: ({ viewportId, button }) => { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return; + } + + const toolName = toolbarService.getToolNameForButton(button); + + if (!toolGroup || !toolGroup.hasTool(toolName)) { + return { + disabled: true, + className: '!text-common-bright ohif-disabled', + disabledText: 'Tool not available', + }; + } + + const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName; + + return { + disabled: false, + className: isPrimaryActive + ? '!text-black bg-primary-light' + : '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light', + }; + }, +}, +``` + +as you can see the job of this evaluator is to determine if the button should be disabled or not. It does so by checking the `toolGroup` and the `toolName` and then returns an object with `disabled` and `className` properties. + +The following evaluators are provided by us: + +- `evaluate.cornerstoneTool`: If assigned to a button (see next), it will make the button react to the active viewport state based on its toolGroup. +- `evaluate.cornerstoneTool.toggle`: It is designed to consider tools with toggle behavior, such as reference lines and image overlay (either on or off). +- `evaluate.cornerstone.synchronizer`: This is designed to consider the synchronizer state of the viewport, whether it is synced or not. +- `evaluate.viewportProperties.toggle`: Some properties of the viewport are toggleable, such as invert, flip, rotate, etc. By assigning this evaluator to those buttons, they will react to the active viewport state based on its properties. This allows for dynamic buttons that change their appearance based on the active viewport state. +- `evaluate.mpr`: special evaluator for MPR since it needs to check if the displaySet is reconstructable or not. + + +Sometime you want to use the same `evaluator` for different purposes, in that case you can use an object +with `name` and other properties. For example, in `'evaluate.cornerstone.segmentation'` we use +this pattern, where multiple toolbar buttons are using the same evaluator but with different options ( + in this case `toolNames` +) + +```js +{ + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleBrush' , 'SphereBrush'] +}, +``` + +#### Composing evaluators + +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'], +``` + +You can even come up with advanced evaluators such as: + +```js +evaluate: [ + 'evaluate.cornerstone.segmentation', + // need to put the disabled text last, since each evaluator will + // merge the result text into the final result + { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, +], +``` + +that we use for our RectangleROIStartEndThreshold tool in tmtv mode. + +As you see this evaluator is composed of two evaluators, one is `evaluate.cornerstone.segmentation` which makes sure (in the implementation), that +there is a segmentation created, and the second one is `evaluate.cornerstoneTool` which makes sure that the tool is available in the viewport. + +Since we are using multiple evaluators, the `disabledText` of each evaluator will be merged into the final result, so you need to +put the `disabledText` in the last evaluator. + +#### Group evaluators +Split buttons (see in [ToolbarService](../../services/data/ToolbarService.md) on how to define one) may feature a group evaluator, we provide two of them and you can write your own. + + +- `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`: determine the outcome of user interactions with the split buttons on what button should be promoted to the primary section. In the example above, the cornerstone tool's status is checked, and if it is not active in the list of buttons, the button is promoted to the primary section. +- `evaluate.group.promoteToPrimary`: disregarding the cornerstone tool's status and promoting the button to the primary section regardless. + +Failure to specify a group evaluator will result in no action, leaving the button in the secondary section. + +:::note +As you have learned so far, the extension modules only 'provides' the functionality +and it is the mode's job to consume it. You can next learn how to consume these components +and evaluators to build a toolbar in the +::: + + +#### Custom Evaluators +You can create your own evaluators. For instance, you have the option to design tri-state buttons, which are buttons with three states such as Show All, Show Some, or Show None of the Viewport Overlays. + + +## Toolbar buttons consumed in modes +Providing just the components is not enough. You need to add the buttons to the toolbar service and decide which ones are used for each section. + +Below we can see a simplified version of the `longitudinal` (basic viewer) mode that shows how +a mode can add buttons to the toolbar by calling +`ToolBarService.addButtons(toolbarButtons)`. `toolbarButtons` is an array of +`toolDefinitions` which we will learn next. + +```js +function modeFactory({ modeConfiguration }) { + return { + id: 'viewer', + displayName: 'Basic Viewer', + + onModeEnter: ({ servicesManager, extensionManager }) => { + const { toolBarService } = servicesManager.services; + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'info', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); + }, + routes: [ + { + path: 'longitudinal', + layoutTemplate: ({ location, servicesManager }) => { + return { + /* */ + }; + }, + }, + ], + }; +} +``` + + +:::note +By default OHIF's default layout (`extensions/default/src/ViewerLayout/index.tsx`) which is used in all modes use a Toolbar component that creates a +`primary` section for tools. That is why we are creating a `primary` section in the example above. + +Layouts are also customizable, and you can create your own layout in your extensions and provide it to your modes view `getLayoutTemplateModule` module. + +By default we use `@ohif/extension-default.layoutTemplateModule.viewerLayout` to use the default layout which provides a + +- Header (with logo on left, toolbar in the middle and user menu on the right) +- Left panel +- Main viewport grid area +- Right panel +::: + + +## Alternative Toolbar sections + +In your UI component, such as panels, you have the option to include a toolbar section template. +This allows you to easily add buttons to it later on. To ensure that the buttons are added properly +to the toolbar, respond to interactions correctly, and evaluate states accurately, simply utilize the `useToolbar` hook. +This hook grants you access to the `onInteraction` function and the `toolbarButtons` array, which you can customize within your UI as needed. + +```js + +function myCustomPanel({servicesManager}){ + const { onInteraction, toolbarButtons } = useToolbar({ + servicesManager, + buttonSection: 'myCustomSectionName' + }); + + // map the buttons to the UI + return ( +
+ {toolbarButtons.map((button, index) => { + return ( + + ); + })} +
+ ); +} + +``` + +We have provided a common component for toolbar buttons called `Toolbox`. +The Toolbox component serves as a versatile and configurable container for toolbar tools within your application. +It is designed to work in conjunction with the useToolbar hook to manage tool states, handle user interactions, and memorize options +using context API. + + +The `Toolbox` can be easily integrated into your application UI, requiring only the necessary services (servicesManager, commandsManager) and configuration parameters (buttonSectionId, title). Here's a simple usage scenario: + + + +```js +function MyApplication({ servicesManager, commandsManager }) { + // Configuration for the toolbox container + const config = { + servicesManager, + commandsManager, + buttonSectionId: 'customButtonSection', + title: 'My Toolbox', + }; + + return ; +} +``` + +Then in your modes you can edit the tools in that button section. + +```js + +onModeEnter: ({ servicesManager, extensionManager }) => { + const { toolBarService } = servicesManager.services; + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('customButtonSection', [ + 'MeasurementTools', + 'Zoom', + 'info', + ]); +}, +``` + +Another example might be you want to open a modal to show some tool options when a button is clicked. +You can use this pattern + + +```js +// ToolbarButton in mode + { + id: 'Others', + uiType: 'ohif.radioGroup', + props: { + icon: 'info-action', + label: 'Others', + commands: 'showOthersModal', + }, + }, +``` + +and inside your mode factory + +```js +// adding the 'Others' button to the primary section +toolbarService.createButtonSection('primary', [ + 'Others', // --------> this one +]); + +// adding the shapes button to the 'Other' section +toolbarService.createButtonSection('other', ['Shapes']); +``` + +here as you see we are using a command `showOthersModal` which is defined in the commands module. + +```js +// inside commandsModule of your extension + showOthersModal: () => { + const { uiModalService } = servicesManager.services; + uiModalService.show({ + content: OthersModal, + title: 'Others', + customClassName: 'w-8', + movable: true, + contentProps: { + onClose: uiModalService.hide, + servicesManager, + commandsManager, + }, + containerDimensions: 'h-[125px] w-[300px]', + contentDimensions: 'h-[125px] w-[300px]', + }); + }, +``` + +as you see it is opening a modal with `OthersModal` component (below) which contains the +`Toolbox` component. + +```js +// Others modal +import { Toolbox } from '@ohif/ui'; + +function OthersModal({ servicesManager, commandsManager }) { + return ( +
+ +
+ ); +} +``` + +The result would be a modal with a toolbox inside it when the `Others` button is clicked, and the +state will get synchronized with the toolbar service automatically. + + +![alt text](../../../assets/img/toolbox-modal.png) + +## Toolbox With Options + +Your toolbox toolbar buttons can have options, this is really useful +for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D). + +:::note +Toolbox with options will run the options commands +on the mount of the toolbox component. This is useful for setting the initial state of the toolbox. +::: + +Currently we support three types of options. + +### Radio option + +We use this in segmentation shapes to let the user choose between +three different modes + +```js +{ + id: 'Shapes', + uiType: 'ohif.radioGroup', + props: { + label: 'Shapes', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], + }, + icon: 'icon-tool-shape', + options: [ + { + name: 'Shape', + type: 'radio', + value: 'CircleScissor', + id: 'shape-mode', + values: [ + { value: 'CircleScissor', label: 'Circle' }, + { value: 'SphereScissor', label: 'Sphere' }, + { value: 'RectangleScissor', label: 'Rectangle' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, +}, +``` + +### Range option + +We use this for brush radius change + +```js +{ + id: 'Brush', + icon: 'icon-tool-brush', + label: 'Brush', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularBrush', 'SphereBrush'], + disabledText: 'Create new segmentation to enable this tool.', + }, + options: [ + { + name: 'Radius (mm)', + id: 'brush-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, + }, + }, + ], +}, +``` + +### Custom option + +We use this pattern inside `tmtv` mode for `RectangleROIThreshold` + +```js +{ + id: 'RectangleROIStartEndThreshold', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-create-threshold', + label: 'Rectangle ROI Threshold', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, + options: 'tmtv.RectangleROIThresholdOptions', + }, +}, +``` + +Note that it is your job to provide the `tmvt.RectangleROIThresholdOptions` in the getToolbarModule of your extension + + + +## Change Toolbar with hanging protocols + +If you want to change the toolbar based on the hanging protocol, you can do a pattern like this. + +```js + + const { unsubscribe } = hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.PROTOCOL_CHANGED, + () => { + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + ]); + } +); +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/utility.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/utility.md new file mode 100644 index 000000000..fbb975d1a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/utility.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 9 +sidebar_label: Utility +--- + +# Module: Utility + +## Overview + +Often, an extension will need to expose some useful functionality to the other +extensions, or modes that consume the extension. For example, the `cornerstone` +extension, uses its `utility` module to expose methods via + +```js +getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + CornerstoneViewportService, + dicomLoaderService, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + ]; + }, +}; +``` + +Then a consuming extension can use `getModuleEntry` to access the methods +Below, which is a code from `TrackedCornerstoneViewport` use the `getUtilityModule` method to get the internal `CornerstoneViewportService` which handles the `Cornerstone` viewport. + +```js title="extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx" +const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' +); + +const { CornerstoneViewportService } = utilityModule.exports; +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/viewport.md b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/viewport.md new file mode 100644 index 000000000..b64a13e33 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/extensions/modules/viewport.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 5 +sidebar_label: Viewport +--- + +# Module: Viewport + +## Overview + +Viewports consume a displaySet and display/allow the user to interact with data. +An extension can register a Viewport Module by defining a `getViewportModule` +method that returns a React component. Currently, we use viewport components to +add support for: + +- 2D Medical Image Viewing (cornerstone ext.) +- Structured Reports as SR (DICOM SR ext.) +- Encapsulated PDFs as PDFs (DICOM pdf ext.) + +The general pattern is that a mode can define which `Viewport` to use for which +specific `SOPClassHandlerUID`, so if you want to fork just a single Viewport +component for a specialized mode, this is possible. + +```jsx +// displaySet, dataSource +const getViewportModule = () => { + const wrappedViewport = props => { + return ( + { + commandsManager.runCommand('commandName', data); + }} + /> + ); + }; + + return [{ name: 'example', component: wrappedViewport }]; +}; +``` + +## Example Viewport Component + +A simplified version of the tracked `OHIFCornerstoneViewport` is shown below, which +creates a cornerstone viewport: + + +```jsx +function TrackedCornerstoneViewport({ + children, + dataSource, + displaySets, + viewportId, + servicesManager, + extensionManager, + commandsManager, +}) { + + return ( +
+ /** Resize Detector */ + + /** Div For displaying image */ +
e.preventDefault()} + onMouseDown={e => e.preventDefault()} + ref={elementRef} + >
+
+ ); +} +``` + +### Viewport re-rendering optimizations + +We make use of the React memoization pattern to prevent unnecessary re-renders +for the viewport unless certain aspects of the Viewport props change. You can take +a look into the `areEqual` function in the `OHIFCornerstoneViewport` component to +see how this is done. + +```js +function areEqual(prevProps, nextProps) { + if (prevProps.displaySets.length !== nextProps.displaySets.length) { + return false; + } + + if ( + prevProps.viewportOptions.orientation !== + nextProps.viewportOptions.orientation + ) { + return false; + } + + // rest of the code +``` + +as you see, we check if the `needsRerendering` prop is true, and if so, we will +re-render the viewport if the `displaySets` prop changes or the orientation +changes. + + +We use viewportId to identify a viewport and we use it as a key in React +rendering. This is important because it allows us to keep track of the viewport +and its state, and also let React optimize and move the viewport around in the +grid without re-rendering it. However, there are some cases where we need to +force re-render the viewport, for example, when the viewport is hydrated +with a new Segmentation. For these cases, we use the `needsRerendering` prop +to force re-render the viewport. You can add it to the `viewportOptions` + + + + + +### `@ohif/app` + +Viewport components are managed by the `ViewportGrid` Component. Which Viewport +component is used depends on: + +- Hanging Protocols +- The Layout Configuration +- Registered SopClassHandlers + +![viewportModule-layout](../../../assets/img/viewportModule-layout.png) + +
An example of three cornerstone Viewports
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/viewer/internationalization.md b/platform/docs/versioned_docs/version-3.8/platform/internationalization.md similarity index 58% rename from platform/docs/versioned_docs/version-2.0-deprecated/viewer/internationalization.md rename to platform/docs/versioned_docs/version-3.8/platform/internationalization.md index aa1136b34..8caf9a729 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/viewer/internationalization.md +++ b/platform/docs/versioned_docs/version-3.8/platform/internationalization.md @@ -2,19 +2,13 @@ sidebar_position: 4 sidebar_label: Internationalization --- + # Viewer: Internationalization OHIF supports internationalization using [i18next](https://www.i18next.com/) through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n), where is the main instance of i18n containing several languages and tools. - -

Our translation management is powered by Locize @@ -24,6 +18,14 @@ where is the main instance of i18n containing several languages and tools.

+## How to change language for the viewer? + +You can take a look into user manuals to see how to change the viewer's +language. In summary, you can change the language: + +- In the preference modals +- Using the language query in the URL: `lng=Test-LNG` + ## Installing ```bash @@ -72,35 +74,24 @@ This section will introduce you to [react-i18next](https://react.i18next.com/) basics and show how to implement the [t](https://www.i18next.com/overview/api#t) function easily. -#### Using HOCs +#### Using Hooks -In most cases we used -[High Order Components](https://react.i18next.com/latest/withtranslation-hoc) to -share the `t` function among OHIF's components. +You can use `useTranslation` hooks that is provided by `react-i18next` -E.g. +You can read more about this +[here](https://react.i18next.com/latest/usetranslation-hook). ```js import React from 'react'; -import { withTranslation } from '@ohif/i18n'; +import { useTranslation } from 'react-i18next'; + +function MyComponent() { + const { t } = useTranslation(); -function MyComponent({ t, i18n }) { return

{t('my translated text')}

; } - -export default withTranslation('MyNameSpace')(MyComponent); ``` -> Important: if you are using React outside the OHIF Viewer, check the -> [I18nextProvider](#using-outside-of-ohif-viewer) section, `withTranslation` -> HOC doesn't works without a I18nextProvider - -#### Using Hooks - -Also, it's possible to get the `t` tool using -[React Hooks](https://react.i18next.com/latest/usetranslation-hook), but it -requires at least React > 16.8 😉 - ### Using outside of OHIF viewer OHIF Viewer already sets a main @@ -112,7 +103,7 @@ all. But, if you need to use it completely outside of OHIF viewer, you can set the I18nextProvider this way: -```js +```jsx import i18n from '@ohif/i18n'; import { I18nextProvider } from 'react-i18next'; import App from './App'; @@ -142,7 +133,7 @@ console.log(T('$t(Common:Play) my translated text')); --- -# Main Concepts While Translating +## Main Concepts While Translating ## Namespaces @@ -151,11 +142,15 @@ semantically or by use. Each `.json` file inside `@ohif/i18n` npm package becomes a new namespace automatically. - Buttons: All buttons translations -- CineDialog: Translations for the toll tips inside the Cine Player Dialog +- CineDialog: Translations for the tool tips inside the Cine Player Dialog - Common: all common jargons that can be reused like `t('$t(common:image)')` - Header: translations related to OHIF's Header Top Bar - MeasurementTable - Translations for the `@ohif/ui` Measurement Table - UserPreferencesModal - Translations for the `@ohif/ui` Preferences Modal +- Modals - Translations available for other modals +- PatientInfo - Translations for patients info hover +- SidePanel - Translations for side panels +- ToolTip - Translations for tool tips ### How to use another NameSpace inside the current NameSpace? @@ -163,7 +158,7 @@ i18next provides a parsing feature able to get translations strings from any NameSpace, like this following example getting data from `Common` NameSpace: ``` -$t(Common:Reset) +$t('Common:Reset') ``` ## Extending Languages in @ohif/i18n @@ -201,21 +196,21 @@ languages tool. You will need to export all Json files in your `index.js` file, mounting an object like this: -```json -{ - en: { - NameSpace: { - keyWord1: 'keyWord1Translation', - keyWord2: 'keyWord2Translation', - keyWord3: 'keyWord3Translation', - } - }, - 'en-UK': { - NameSpace: { - keyWord1: 'keyWord1DifferentTranslation', - } - } -} +```js + { + en: { + NameSpace: { + keyWord1: 'keyWord1Translation', + keyWord2: 'keyWord2Translation', + keyWord3: 'keyWord3Translation', + } + }, + 'en-UK': { + NameSpace: { + keyWord1: 'keyWord1DifferentTranslation', + } + } + } ``` Please check the `index.js` files inside locales folder for an example of this @@ -248,7 +243,7 @@ To set a brand new language you can do it in two different ways: - Setting it only in your project or extension: -You'll need a a final object like the following, what is setting French as +You'll need a final object like the following, what is setting French as language, and send it to `addLocales` method. ```js @@ -284,6 +279,15 @@ You can also set them manually, one by one, using this --- +## Test Language + +We have created a test language that its translations can be seen in the locales +folder. You can copy paste the folder and its `.json` namespaces and add your +custom language translations. + +> If you apply the test-LNG you can see all the elements get appended with 'Test +> {}'. For instance `Study list` becomes `Test Study list`. + ## Language Detections @ohif/i18n uses @@ -320,7 +324,73 @@ Run the project as following to get full debug information: REACT_APP_I18N_DEBUG=true yarn run dev ``` -### Contributing with new languages +## Contributing with new languages -Contributions of any kind are welcome! Please check the -[instructions](../development/contributing.md). +We have integrated `i18next` into the OHIF Viewer and hooked it up with Locize +for translation management. Now we need your help to get the app translated into +as many languages as possible, and ensure that we haven't missed pieces of the +app that need translation. Locize has graciously offered to provide us with free +usage of their product. + +Once each crowd-sourcing project is completed, we can approve it and merge the +changes into the main project. At that point, the language will be immediately +available on https://viewer.ohif.org/ for testing, and can be used in any OHIF +project. We will support usage through both the Locize CDN and by copying the +language directly into the `@ohif/i18n` package, so that end users can serve the +content from their own domains. + +Here are a couple examples: + +Spanish: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=es + +Chinese: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=zh + +Portuguese: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=pt-BR + +Here are some links you can use to sign up to help translate. All you have to do +is sign up, translate the strings, and click Save. On our side, we have a +dashboard to see how many strings are translated and by whom. + +This is a pretty random set of languages, so please post below if you'd like a +new language link to be added: + +Languages: + +[French](https://www.locize.io/register?invitation=Nj8jRPaFKYwtIfNZ6Y5GVOJOpeiXNAdVuSiOg9ceaiveP6uF6y1wVXM9lgfKoYZX) + +[German](https://www.locize.io/register?invitation=gChNiVi66YINTPpbKESVAVYPapwg3DkpvMSSomLTvVqBJTXrdmPvxi0WZYHER11q) + +[Dutch](https://www.locize.io/register?invitation=2PGe7I184aN0cazM4GXMhzeLtGTf9Zen5uyOEFhHQ8vYkfKHkgR0mJ8dwbNlIeCG) + +[Turkish](https://www.locize.io/register?invitation=NOMIXsfneqPbFDqjce5wI7Z6p2swXSjc0rHOH4KLcM6qXSNA4LGyJaLxS7nqWAe3) + +[Chinese](https://www.locize.io/register?invitation=lrcUbt7DvV4aJmQeEA4SMAj5xNWr3rltOcaZW1cFc6eod0nvzSPFU4V383tDHGGn) + +[Japanese](https://www.locize.io/register?invitation=AaRq2S22o5FsxArwgVuw1gZcQjoe2ffyxarqlAXOpN7JnR2sf2mfamc5qV6LG1Mn) + +[Arabic](https://www.locize.io/register?invitation=BiqI6fOm1sC84N3YJLbImXmaOCk8Hc3TMGpXg7NH2R0b0OKuPCp9wlCHLoqMRpfQ) + +[Hindi](https://www.locize.io/register?invitation=ph7JmOGTV95DF3EFaI1kvK5Hx98dV9w2wj9h9UhUCWnkBNAwWEdWMcyjnF94zkWb) + +[Malay](https://www.locize.io/register?invitation=HsV9F5mKZyeUZYrC3XFRzNI2l0EsIh6hK0MUIKP8IYZA3GxuzfgkvWBLCFwCpDik) + +[Russian](https://www.locize.io/register?invitation=da4V9Q8DVO3M1FIlvfT50ZiS8NDNgvC0dE5hHUEAp47FXy6pLXmf1cp2lgLBfLmb) + +[Swedish](https://www.locize.io/register?invitation=uR4kzBZC1vhJe6jyMwYXgGPj84QDMulQRlt2s6rONU6ljUh5dgwuUyhJEtZ4REA3) + +[Italian](https://www.locize.io/register?invitation=viAS1NC5q342OxtuIv3JFX9DJ3KoR4SmGoElkBlRMphsDKt4hy9bW8JfBjHlfnd7) + +[Spanish](https://www.locize.io/register?invitation=ZikXW3KI4w4eo5Cf6L1aQMWaR69XAQ0a9Va3NGorH7mAPvEPXp8w8NLkPNLs5nG8) + +[Ukrainian](https://www.locize.io/register?invitation=TY0s6onqH3Asl05Bh1qB44SNSABL2pTYoturwxAmcNKRnzBZFK7bGfn7kVi23Vpg) + +[Vietnamese](https://www.locize.io/register?invitation=eqfHDm0vaqxGfQ5TGt6SeV0dx9b2dCp1RrMRdIRavqzOCOAfD3IElzUsyIT689cK) + +[Portugese-Brazil](https://www.locize.io/register?invitation=Qc5Dq449xbblQqLTpWeMfsyFiu3gACcgpj0EIucQjjs9Ph9pzPLpq3MnZupF9t6N) + +Don't see your language in the above list? Add a request +[here](https://github.com/OHIF/Viewers/issues/618) so that we can create the +language for your translation contribution. diff --git a/platform/docs/versioned_docs/version-3.8/platform/managers/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/managers/_category_.json new file mode 100644 index 000000000..f2a2e07a2 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/managers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Managers", + "position": 10 +} diff --git a/platform/docs/versioned_docs/version-3.8/platform/managers/commands.md b/platform/docs/versioned_docs/version-3.8/platform/managers/commands.md new file mode 100644 index 000000000..1f07f31a9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/managers/commands.md @@ -0,0 +1,180 @@ +--- +sidebar_position: 4 +sidebar_label: Commands Manager +--- +# Commands Manager + +## Overview + + +The `CommandsManager` is a class defined in the `@ohif/core` project. The Commands Manager tracks named commands (or functions) that are scoped to +a context. When we attempt to run a command with a given name, we look for it +in our active contexts. If found, we run the command, passing in any application +or call specific data specified in the command's definition. + +> Note: A single instance of `CommandsManager` should be defined in the consuming application, and it is used when constructing the `ExtensionManager`. + +A `simplified skeleton` of the `CommandsManager` is shown below: + +```js +export class CommandsManager { + constructor({ getActiveContexts } = {}) { + this.contexts = {}; + this._getActiveContexts = getActiveContexts; + } + + getContext(contextName) { + const context = this.contexts[contextName]; + return context; + } + + /**...**/ + + createContext(contextName) { + /** ... **/ + this.contexts[contextName] = {}; + } + + + registerCommand(contextName, commandName, definition) { + /**...**/ + const context = this.getContext(contextName); + /**...**/ + context[commandName] = definition; + } + + runCommand(commandName, options = {}, contextName) { + const definition = this.getCommand(commandName, contextName); + /**...**/ + const { commandFn } = definition; + const commandParams = Object.assign( + {}, + definition.options, // "Command configuration" + options // "Time of call" info + ); + /**...**/ + return commandFn(commandParams); + } + /**...**/ +} +``` + + + + +### Instantiating + +When we instantiate the `CommandsManager`, we are passing two methods: + +- `getAppState` - Should return the application's state when called (Not implemented in `v3`) +- `getActiveContexts` - Should return the application's active contexts when + called + +These methods are used internally to help determine which commands are currently +valid, and how to provide them with any state they may need at the time they are +called. + +```js title="platform/app/src/appInit.js" +const commandsManagerConfig = { + getAppState: () => {}, + /** Used by commands to determine active context */ + getActiveContexts: () => [ + 'VIEWER', + 'DEFAULT', + 'ACTIVE_VIEWPORT::CORNERSTONE', + ], +}; + +const commandsManager = new CommandsManager(commandsManagerConfig); +``` + + +## Commands/Context Registration +The `ExtensionManager` handles registering commands and creating contexts, so you don't need to register all your commands manually. Simply, create a `commandsModule` in your extension, and it will get automatically registered in the `context` provided. + +A *simplified version* of this registration is shown below to give an idea about the process. + + +```js +export default class ExtensionManager { + constructor({ commandsManager }) { + this._commandsManager = commandsManager + } + /** ... **/ + registerExtension = (extension, configuration = {}, dataSources = []) => { + let extensionId = extension.id + /** ... **/ + + // Register Modules provided by the extension + moduleTypeNames.forEach((moduleType) => { + const extensionModule = this._getExtensionModule( + moduleType, + extension, + extensionId, + configuration + ) + + if (moduleType === 'commandsModule') { + this._initCommandsModule(extensionModule) + } + /** registering other modules **/ + }) + } + + _initCommandsModule = (extensionModule) => { + let { definitions, defaultContext } = extensionModule + defaultContext = defaultContext || 'VIEWER' + + if (!this._commandsManager.getContext(defaultContext)) { + this._commandsManager.createContext(defaultContext) + } + + Object.keys(definitions).forEach((commandName) => { + const commandDefinition = definitions[commandName] + const commandHasContextThatDoesNotExist = + commandDefinition.context && + !this._commandsManager.getContext(commandDefinition.context) + + if (commandHasContextThatDoesNotExist) { + this._commandsManager.createContext(commandDefinition.context) + } + + this._commandsManager.registerCommand( + commandDefinition.context || defaultContext, + commandName, + commandDefinition + ) + }) + } +} + +``` + + +If you find yourself in a situation where you want to register a command/context manually, ask +yourself "why can't I register these commands via an extension?", but if you insist, you can use the `CommandsManager` API to do so: + +```js +// Command Registration +commandsManager.registerCommand('context', 'name', commandDefinition); + +// Context Creation +commandsManager.createContext('string'); +``` + +## `CommandsManager` Public API + +If you would like to run a command in the consuming app or an extension, you can +use `runCommand(commandName, options = {}, contextName)`. + + +```js +// Run a command, it will run all the `speak` commands in all contexts +commandsManager.runCommand('speak', { command: 'hello' }); + +// Run command, from Default context +commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']); + +// Returns all commands for a given context +commandsManager.getContext('string'); +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/managers/extension.md b/platform/docs/versioned_docs/version-3.8/platform/managers/extension.md new file mode 100644 index 000000000..a31f22442 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/managers/extension.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +sidebar_label: Extension Manager +--- + +# Extension Manager + +## Overview + +The `ExtensionManager` is a class made available to us via the `@ohif/core` +project (platform/core). Our application instantiates a single instance of it, +and provides a `ServicesManager` and `CommandsManager` along with the +application's configuration through the appConfig key (optional). + +```js +const commandsManager = new CommandsManager(); +const servicesManager = new ServicesManager(); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + appConfig, +}); +``` +## Events +The following events get published by the `ExtensionManager`: + +| Event | Description | +| ---------------------------- | ------------------------------------------------------ | +| ACTIVE_DATA_SOURCE_CHANGED | Fired when the active data source is changed - either replaced with an entirely different one or the existing active data source gets its definition changed via `updateDataSourceConfiguration`. | + +## API +The `ExtensionManager` only has the following public API: + +- `setActiveDataSource` - Sets the active data source for the application +- `getDataSources` - Returns the registered data sources +- `getActiveDataSource` - Returns the currently active data source +- `getModuleEntry` - Returns the module entry by the give id. +- `addDataSource` - Dynamically adds a data source and optionally sets it as the active data source +- `updateDataSourceConfiguration` - Updates the configuration of a specified data source (name). +- `getDataSourceDef` - Gets the data source definition for a particular data source name. + +## Accessing Modules + +We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on +the provided IDs in the mode's configuration. + +For instance: +`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")` +accesses the `seriesList` panel from `panelModule` of the +`@ohif/extension-measurement-tracking` extension. + +```js +const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; +}; +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/managers/hotkeys.md b/platform/docs/versioned_docs/version-3.8/platform/managers/hotkeys.md new file mode 100644 index 000000000..ebdd2bd13 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/managers/hotkeys.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 5 +sidebar_label: Hotkeys Manager +--- +# Hotkeys Managers + +## Overview +`HotkeysManager` handles all the logics for adding, setting and enabling/disabling +the hotkeys. + + + +## Instantiation +`HotkeysManager` is instantiated in the `appInit` similar to the other managers. + +```js +const commandsManager = new CommandsManager(commandsManagerConfig); +const servicesManager = new ServicesManager(commandsManager); +const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + hotkeysManager, + appConfig, +}); +``` + + + + +## Hotkeys Manager API + +- `setHotkeys`: The most important method in the `HotkeysManager` which binds the keys with commands. +- `setDefaultHotKeys`: set the defaultHotkeys **property**. Note that, this method **does not** bind the provided hotkeys; however, when `restoreDefaultBindings` +is called, the provided defaultHotkeys will get bound. +- `destroy`: reset the HotkeysManager, and remove the set hotkeys and empty out the `defaultHotkeys` + + + +## Structure of a Hotkey Definition +A hotkey definition should have the following properties: + +- `commandName`: name of the registered command +- `commandOptions`: extra arguments to the commands +- `keys`: an array defining the key to get bound to the command +- `label`: label to be shown in the hotkeys preference panel +- `isEditable`: whether the key can be edited by the user in the hotkey panel + + +### Default hotkeysBindings +The default key bindings can be find in `hotkeyBindings.js` + +```js +// platform/core/src/defaults/hotkeyBindings.js + +export default [ + /**..**/ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + + { + commandName: 'flipViewportHorizontal', + label: 'Flip Vertically', + keys: ['v'], + isEditable: true, + }, + /**..**/ +] +``` + + +## Behind the Scene +When you `setHotkeys`, the `commandName` gets registered with the `commandsManager` and +get run after the key is pressed. diff --git a/platform/docs/versioned_docs/version-3.8/platform/managers/index.md b/platform/docs/versioned_docs/version-3.8/platform/managers/index.md new file mode 100644 index 000000000..ee6d6b2fc --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/managers/index.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Managers + +## Overview + +`OHIF` uses `Managers` to accomplish various purposes such as registering new +services, dependency injection, and aggregating and exposing `extension` +features. + +`OHIF-v3` provides the following managers which we will discuss in depth. + + + + + + + + + + + + + + + + + + + + + + + + + + +
ManagerDescription
+ + Extension Manager + + + Aggregating and exposing modules and features through out the app +
+ + Services Manager + + + Single point of registration for all internal and external services +
+ + Commands Manager + + + Register commands with specific context and run commands in the app +
+ + Hotkeys Manager + + + For keyboard keys assignment to commands +
+ + + + + +[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services +[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js +[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern + diff --git a/platform/docs/versioned_docs/version-3.8/platform/managers/service.md b/platform/docs/versioned_docs/version-3.8/platform/managers/service.md new file mode 100644 index 000000000..d9269e46c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/managers/service.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 3 +sidebar_label: Service Manager +--- + +# Services Manager + +## Overview + +Services manager is the single point of service registration. Each service needs +to implement a `create` method which gets called inside `ServicesManager` to +instantiate the service. In the app, you can get access to a registered service +via the `services` property of the `ServicesManager`. + +## Skeleton + +_Simplified_ skeleton of `ServicesManager` is shown below. There are two public +methods: + +- `registerService`: registering a new service with/without a configuration +- `registerServices`: registering batch of services + +```js +export default class ServicesManager { + constructor(commandsManager) { + this._commandsManager = commandsManager; + this.services = {}; + this.registeredServiceNames = []; + } + + registerService(service, configuration = {}) { + /** validation checks **/ + this.services[service.name] = service.create({ + configuration, + commandsManager: this._commandsManager, + }); + + /* Track service registration */ + this.registeredServiceNames.push(service.name); + } + + registerServices(services) { + /** ... **/ + } +} +``` + +## Default Registered Services + +By default, `OHIF-v3` registers the following services in the `appInit`. + +```js title="platform/app/src/appInit.js" +servicesManager.registerServices([ + CustomizationService, + UINotificationService, + UIModalService, + UIDialogService, + UIViewportDialogService, + MeasurementService, + DisplaySetService, + ToolBarService, + ViewportGridService, + HangingProtocolService, + CineService, +]); +``` + +## Service Architecture + +If you take a look at the folder of each service implementation above, you will +find out that services need to be exported as an object with `name` and `create` +method. + +For instance, `ToolBarService` is exported as: + +```js title="platform/core/src/services/ToolBarService/index.js" +import ToolBarService from './ToolBarService'; + +export default { + name: 'ToolBarService', + create: ({ configuration = {}, commandsManager }) => { + return new ToolBarService(commandsManager); + }, +}; +``` + +and the implementation of `ToolBarService` lies in the same folder at +`./ToolbarSerivce.js`. + +> Note: The create method is critical for any custom service that you write and +> want to add to the list of services + +> Note: For typescript definitions, the service type should be exported +> as part of the Types export on the module. This is recommended going forward +> and existing services will be migrated. As well, the capitalization of the +> name should be lower camel case, with the type being upper camel case. In +> the above example, the service instance should be `toolBarService` with the +> class being `ToolBarService`. + +## Accessing Services + +Throughout the app you can use `services` property of the service manager to +access the desired service. + +For instance in the `PanelMeasurementTableTracking` which is the right panel in +the `longitudinal` mode, we have the _simplified code below_ for downloading the +drawn measurements. + +```js +function PanelMeasurementTableTracking({ servicesManager }) { + const { MeasurementService } = servicesManager.services; + /** ... **/ + + async function exportReport() { + const measurements = MeasurementService.getMeasurements(); + /** ... **/ + downloadCSVReport(measurements, MeasurementService); + } + + /** ... **/ + return <> /** ... **/ ; +} +``` + +## Registering Custom Services + +You might need to write you own custom service in an extension. +`preRegistration` hook inside your extension is the place for registering your +custom service. + +```js title="extensions/customExtension/src/index.js" +import WrappedBackEndService from './services/backEndService'; + +export default { + // ID of the extension + id: 'myExtension', + preRegistration({ servicesManager }) { + servicesManager.registerService(WrappedBackEndService(servicesManager)); + }, +}; +``` + +and the logic for your service shall be + +```js title="extensions/customExtension/src/services/backEndService/index.js" +// Canonical name of upper camel case BackEndService for the class +import BackEndService from './BackEndService'; + +export default function WrappedBackEndService(servicesManager) { + return { + // Note the canonical name of lower camel case backEndService + name: 'backEndService', + create: ({ configuration = {} }) => { + return new BackEndService(servicesManager); + }, + }; +} +``` + +with implementation of + +```ts +export default class BackEndService { + constructor(servicesManager) { + this.servicesManager = servicesManager; + } + + putAnnotations() { + return post(/*...*/); + } +} +``` + +with a registration of + +```ts title="types/index.ts" +import BackEndService from "../services/BackEndService/BackEndService"; + +export { BackEndService }; +``` + +# Service Mode Lifecycle +Services may implement initialization and cleanup for mode specific data. +In order to prevent defects where there are differences between initial +and subsequent displays of a study, the contract of the service is that the +state the service is in on mode entry shall be the same whether the mode was +entered or was exited and entered again. + +To implement storage/recovery of state, the mode must store the data on +exiting the mode, and restore the data in it's onModeEnter. For example, +the mode may decide to preserve measurement data in the onModeExit, and +to restore it in the onModeEnter. This does not violate the contract since +it is the mode's decision to apply the stored state, and to cache it. + +## onModeEnter +A service may implement an onModeEnter call to initialize the service to +be ready for entering a mode. +This is called before the mode `onModeEnter` is called. + +## onModeExit +When entering a mode, the service contract states that the service needs to +be in the same state whether it is a fresh load or has previously entered the mode. +The onModeExit allows a service to clean itself up after the mode 'onModeExit' +has stored any persistent data. diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/faq/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/modes/_category_.json similarity index 51% rename from platform/docs/versioned_docs/version-1.0-deprecated/faq/_category_.json rename to platform/docs/versioned_docs/version-3.8/platform/modes/_category_.json index 0fa10da5c..889d84503 100644 --- a/platform/docs/versioned_docs/version-1.0-deprecated/faq/_category_.json +++ b/platform/docs/versioned_docs/version-3.8/platform/modes/_category_.json @@ -1,4 +1,4 @@ { - "label": "FAQ", + "label": "Modes", "position": 12 } diff --git a/platform/docs/versioned_docs/version-3.8/platform/modes/index.md b/platform/docs/versioned_docs/version-3.8/platform/modes/index.md new file mode 100644 index 000000000..aded22bd0 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/modes/index.md @@ -0,0 +1,413 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Modes + +## Overview + +A mode can be thought of as a viewer app configured to perform a specific task, +such as tracking measurements over time, 3D segmentation, a guided radiological +workflow, etc. Addition of modes enables _application_ with many _applications_ +as each mode become a mini _app configuration_ behind the scene. + +Upon initialization the viewer will consume extensions and modes and build up +the route desired, these can then be accessed via the study list, or directly +via url parameters. + + + +OHIF-v3 architecture can be seen in the following: + +![mode-archs](../../assets/img/mode-archs.png) + +> Note: Templates are now a part of “extensions” Routes are configured by modes +> and/or app + +As mentioned, modes are tied to a specific route in the viewer, and multiple +modes/routes can be present within a single application. This allows for +tremendously more flexibility than before you can now: + +- Simultaneously host multiple viewers with for different use cases from within + the same app deploy. +- Make radiological viewers for specific purposes/workflows, e.g.: + - Tracking the size of lesions over time. + - PET/CT fusion workflows. + - Guided review workflows optimized for a specific clinical trial. +- Still host one single feature-rich viewer if you desire. + +## Anatomy + +A mode configuration has a `route` name which is dynamically transformed into a +viewer route on initialization of the application. Modes that are available to a +study will appear in the study list. + +![user-study-summary](../../assets/img/user-study-summary.png) + +The mode configuration specifies which `extensions` the mode requires, which +`LayoutTemplate` to use, and what props to pass to the template. For the default +template this defines which `side panels` will be available, as well as what +`viewports` and which `displaySets` they may hang. + +Mode's config is composed of three elements: +- `id`: the mode `id` +- `modeFactory`: the function that returns the mode specific configuration +- `extensionDependencies`: the list of extensions that the mode requires + + +that return a config object with certain +properties, the high-level view of this config object is: + +```js title="modes/example/src/index.js" +function modeFactory() { + return { + id: '', + version: '', + displayName: '', + onModeEnter: () => {}, + onModeExit: () => {}, + validationTags: {}, + isValidMode: () => {}, + routes: [ + { + path: '', + init: () => {}, + layoutTemplate: () => {}, + }, + ], + extensions: extensionDependencies, + hangingProtocol: [], + sopClassHandlers: [], + hotkeys: [] + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
+ id + unique mode id used to refer to the mode
+ displayName + actual name of the mode being displayed for each study in the study summary panel
+ + onModeEnter + + hook is called when the mode is entered by the specified route
+ + onModeExit + + hook is called when the mode exited
+ + validationTags + + validationTags
+ + isValidMode + + Checks if the mode is valid for a study
+ + routes + + route config which defines the route address, and the layout for it
+ + extensionDependencies + + extensions needed by the mode
+ + hanging protocol + + list of hanging protocols that the mode should have access to
+ + sopClassHandlers + + list of SOPClass modules needed by the mode
+ + hotkeys + + hotkeys
+ +### Consuming Extensions + +As mentioned in the [Extensions](../extensions/index.md) section, in `OHIF-v3` +developers write their extensions to create reusable functionalities that later +can be used by `modes`. Now, it is time to describe how the registered +extensions will get utilized for a workflow mode via its `id`. + +Each `mode` has a list of its `extensions dependencies` which are the +the `extension` name and version number. In addition, to use a module element you can use the +`${extensionId}.${moduleType}.${element.name}` schema. For instance, if a mode +requires the left panel with name of `AIPanel` that is added by the +`myAIExtension` via the following `getPanelModule` code, it should address it as +`myAIExtension.panelModule.AIPanel` inside the mode configuration file. In the +background `OHIF` will handle grabbing the correct panel via `ExtensionManager`. + +```js title="extensions/myAIExtension/getPanelModule.js" +import PanelAI from './PanelAI.js'; + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedAIPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'AIPanel', + iconName: 'list-bullets', + iconLabel: '', + label: 'AI Panel', + isDisabled: studies => {}, // optional + component: wrappedAIPanel, + }, + ]; +} +``` + +Now, let's look at a simplified code of the `basic viewer` mode which consumes various functionalities +from different extensions. + +```js + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', +}; + +const id = 'viewer'; +const version = '3.0.0'; + +function modeFactory({ modeConfiguration }) { + return { + id, + // ... + routes: [ + { + // ... + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: ['@ohif/extension-measurement-tracking.panelModule.seriesList'], + rightPanels: ['@ohif/extension-measurement-tracking.panelModule.trackedMeasurements'], + viewports: [ + { + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: ['@ohif/extension-default.sopClassHandlerModule.stack'], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: ['@ohif/extension-default.hangingProtocolModule.petCT'], + sopClassHandlers: ['@ohif/extension-default.sopClassHandlerModule.stack'], + // ... + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +} + +export default mode +``` + +### Routes + +routes config is an array of route settings, and the overall look and behavior +of the viewer at the designated route is defined by the `layoutTemplate` and +`init` functions for the route. We will learn more about each of the above +properties inside the [route documentation](./routes.md) + + +### HangingProtocols + +Currently, you can pass your defined hanging protocols inside the +`hangingProtocols` property of the mode's config. If you specify the hanging protocol +explicitly by its name (only string and not array), it will be THE hanging protocol +that the mode runs with. However, if you specify an array of hanging protocols, +they will get ranked based on the displaySetSelector requirements and the winner +will be the hanging protocol that the mode runs with. + + +### SopClassHandlers + +Mode's configuration also accepts the `sopClassHandler` modules that have been +added by the extensions. This information will get used to initialize `DisplaySetService` with the provided SOPClass modules which +handles creation of the displaySets. + + +### Hotkeys + +`hotkeys` is another property in the configuration of a mode that can be defined +to add the specific hotkeys to the viewer on the mode route. Additionally, the +name under which the hotkeys are stored can be configured as `hotkeyName`. +This allows user customization of the mode specific hotkeys. + +```js +// default hotkeys +import { utils } from '@ohif/ui'; + +const { hotkeys } = utils; + +const myHotkeys = [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + { + commandName: 'scaleUpViewport', + label: 'Zoom In', + keys: ['+'], + isEditable: true, + }, +] + +function modeFactory() { + return { + id: '', + id: '', + displayName: '', + /* + ... + */ + hotkeys: { + // The name in preferences to use for this set of hotkeys + // Allows defining different sets for different modes + name: 'custom-hotkey-name', + // And the actual custom values here. + hotkeys:[..hotkeys.defaults.hotkeyBindings, ...myHotkeys] + }, + } +} + +// exports +``` + + + + + +## Registration + +Similar to extension registration, `viewer` will look inside the `pluginConfig.json` to +find the `modes` to register. + + +```js title=platform/app/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.4.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.4.0" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register modes in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall modes. Read more [here](../../development/ohif-cli.md) +::: + +The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). + + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: diff --git a/platform/docs/versioned_docs/version-3.8/platform/modes/installation.md b/platform/docs/versioned_docs/version-3.8/platform/modes/installation.md new file mode 100644 index 000000000..08c456d29 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/modes/installation.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 5 +sidebar_label: Installation +--- + +# Modes: Installation + +OHIF-v3 provides the ability to utilize external modes. + + +You can use ohif `cli` tool to install both local and publicly published +modes on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/versioned_docs/version-3.8/platform/modes/lifecycle.md b/platform/docs/versioned_docs/version-3.8/platform/modes/lifecycle.md new file mode 100644 index 000000000..8c0ec41ce --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/modes/lifecycle.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 2 +sidebar_label: Lifecycle Hooks +--- + +# Modes: Lifecycle Hooks + +## Overview + +Currently, there are two hooks that are called for modes: + +- onModeInit +- onModeEnter +- onModeExit + +## onModeInit + +This hook gets run before the defined route has been entered by the mode. This +hook can be used for initialization before the first render. + +This is called before `onModeEnter` calls. This allows modes to add or activate their own +data sources and configuration before entering the mode (pre registrations). + +## onModeEnter + +This hook gets run after the defined route has been entered by the mode. This +hook can be used to initialize the data, services and appearance of the viewer +upon the first render, in any way that is custom to the mode. + +This is called after service `onModeEnter` calls so that the entry into a mode +is done in a predefined/fixed state. That allows any restoring of existing state +to be performed. + +For instance, in `longitudinal` mode we are using this hook to initialize the +`ToolBarService` and set the window level/width tool to be active and add +buttons to the toolbar. + +:::note Tip + +In OHIF Version 3.1, there is a new service `ToolGroupService` that is used to +define and manage tools for the group of viewports. This is a new concept +borrowed from the Cornerstone ToolGroup, and you can read more +[here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolgroups/) + +::: + +```js +function modeFactory() { + return { + id: '', + version: '', + displayName: '', + onModeEnter: ({ servicesManager, extensionManager }) => { + const { ToolBarService, ToolGroupService } = servicesManager.services; + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, ToolGroupService); + + ToolBarService.addButtons(toolbarButtons); + ToolBarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'MoreTools', + ]); + }, + /* + ... + */ + }; +} +``` + +## onModeExit + +This hook is called when the viewer navigates away from the route in the url. +It is called BEFORE the service specific onModeExit calls are performed, and +thus still has access to stateful data which can be cached or stored before +the services clean themselves up. +This is the place for cleaning up NON-service specific data, and services +by unsubscribing to the events. The cleanup of the service itself is intended +to occur in the service `onModeEnter`. + +For instance, it can be used to reset the `ToolBarService` which reset the +toggled buttons. + +```js +function modeFactory() { + return { + id: '', + displayName: '', + onModeExit: ({ servicesManager, extensionManager }) => { + // Turn of the toggled states on exit + const { ToolBarService } = servicesManager.services; + ToolBarService.reset(); + }, + /* + ... + */ + }; +} +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/modes/routes.md b/platform/docs/versioned_docs/version-3.8/platform/modes/routes.md new file mode 100644 index 000000000..4f4130164 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/modes/routes.md @@ -0,0 +1,359 @@ +--- +sidebar_position: 3 +sidebar_label: Routes +--- + +# Mode: Routes + +## Overview + +Modes are tied to a specific route in the viewer, and multiple modes/routes can +be present within a single application. This makes `routes` config, THE most +important part of the mode configuration. + +## Route + +`@ohif/app` **compose** extensions to build applications on different routes +for the platform. + +Below, you can see a simplified version of the `longitudinal` mode and the +`routes` section which has defined one `route`. Each route has three different +configuration: + +- **route path**: defines the route path to access the built application for + that route +- **route init**: hook that runs when application enters the defined route path, + if not defined the default init function will run for the mode. +- **route layout**: defines the layout of the application for the specified + route (panels, viewports) + +```js +function modeFactory() { + return { + id: 'viewer', + version: '3.0.0', + displayName: '', + routes: [ + { + path: 'longitudinal', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ + '@ohif/extension-measurement-tracking.panelModule.seriesList', + ], + rightPanels: [ + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + ], + viewports: [ + { + namespace: + '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: [ + '@ohif/extension-default.sopClassHandlerModule.stack', + ], + }, + { + namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ + '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + ], + }, + ], + }, + }; + }, + }, + ], + /* + ... + */ + }; +} +``` + +### Route: path + +Upon initialization the viewer will consume extensions and modes and build up +the route desired, these can then be accessed via the study list, or directly +via url parameters. + +> Note: Currently, only one route is built for each mode, but we will enhance +> route creation to create separate routes based on the `path` config for each +> `route` object. + +There are two types of `routes` that are created by the mode. + +- Routes with dataSourceName `/${mode.id}/${dataSourceName}` +- Routes without dataSourceName `/${mode.id}` + +Therefore, navigating to +`http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` +will run the app with the layout and functionalities of the `viewer` mode using +the `defaultDataSourceName` which is defined in the +[App Config](../../configuration/index.md) + +You can use the same exact mode using a different registered data source (e.g., +`dicomjson`) by navigating to +`http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` + +### Route: init + +The mode also has an init hook, which initializes the mode. If you don't define +an `init` function the `default init` function will get run (logic is located +inside `Mode.jsx`). However, you can define you own init function following +certain steps which we will discuss next. + +#### Default init + +Default init function will: + +- `retriveSeriesMetaData` for the `studyInstanceUIDs` that are defined in the + URL. +- Subscribe to `instanceAdded` event, to make display sets after a series have + finished retrieving its instances' metadata. +- Subscribe to `seriesAdded` event, to run the `HangingProtocolService` on the + retrieves series from the study. + +A _simplified_ "pseudocode" for the `defaultRouteInit` is: + +```jsx +async function defaultRouteInit({ + servicesManager, + studyInstanceUIDs, + dataSource, +}) { + const { + DisplaySetService, + HangingProtocolService, + } = servicesManager.services; + + // subscribe to run the function after the event happens + DicomMetadataStore.subscribe( + 'instancesAdded', + ({ StudyInstanceUID, SeriesInstanceUID }) => { + const seriesMetadata = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + DisplaySetService.makeDisplaySets(seriesMetadata.instances); + } + ); + + studyInstanceUIDs.forEach(StudyInstanceUID => { + dataSource.retrieve.series.metadata({ StudyInstanceUID }); + }); + + DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => { + const studyMetadata = // get study metadata and displaySets + HangingProtocolService.run({studies, displaySets, activeStudy}); + }); + + return unsubscriptions; +} +``` + +#### Writing a custom init + +You can add your custom init function to enhance the default initialization for: + +- Fetching annotations from a server for the current study +- Changing the initial image index of the series to be displayed at first +- Caching the next study in the work list +- Adding a custom sort for the series to be displayed on the study browser panel + +and lots of other modifications. + +You just need to make sure, the mode `dataSource.retrieve.series.metadata`, +`makeDisplaySets` and `run` the HangingProtocols at some point. There are +various `events` that you can subscribe to and add your custom logic. **point to +events** + +For instance for jumping to the slice where a measurement is located at the +initial render, you need to follow a pattern similar to the following: + +```jsx +init: async ({ + servicesManager, + extensionManager, + hotkeysManager, + dataSource, + studyInstanceUIDs, +}) => { + const { DisplaySetService } = servicesManager.services; + + /** + ... + **/ + + const onDisplaySetsAdded = ({ displaySetsAdded, options }) => { + const displaySet = displaySetsAdded[0]; + const { SeriesInstanceUID } = displaySet; + + const toolData = myServer.fetchMeasurements(SeriesInstanceUID); + + if (!toolData.length) { + return; + } + + toolData.forEach(tool => { + const instance = displaySet.images.find( + image => image.SOPInstanceUID === tool.SOPInstanceUID + ); + }); + + MeasurementService.addMeasurement(/**...**/); + }; + + // subscription to the DISPLAY_SETS_ADDED + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, + onDisplaySetsAdded + ); + + /** + ... + **/ + + return unsubscriptions; +}; +``` + +### Route: layoutTemplate + +`layoutTemplate` is the last configuration for a certain route in a `mode`. +`layoutTemplate` is a function that returns an object that configures the +overall layout of the application. The returned object has two properties: + +- `id`: the id of the `layoutTemplate` being used (it should have been + registered via an extension) +- `props`: the required properties to be passed to the `layoutTemplate`. + +For instance `default extension` provides a layoutTemplate that builds the app +using left/right panels and viewports. Therefore, the `props` include +`leftPanels`, `rightPanels` and `viewports` sections. Note that the +`layoutTemplate` defines the properties it is expecting. So, if you write a +`layoutTemplate-2` that accepts a footer section, its logic should be written in +the extension, and any mode that is interested in using `layoutTemplate-2` +**should** provide the `id` for the footer component. + +**What module should the footer be registered?** + +```js +/* +... +*/ +layoutTemplate: ({ location, servicesManager }) => { + return { + id: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + props: { + leftPanels: [ + 'myExtension.panelModule.leftPanel1', + 'myExtension.panelModule.leftPanel2', + ], + rightPanels: ['myExtension.panelModule.rightPanel'], + viewports: [ + { + namespace: 'myExtension.viewportModule.viewport1', + displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop1'], + }, + { + namespace: 'myExtension.viewportModule.viewport2', + displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop2'], + }, + ], + }, + }; +}; +/* +... +*/ +``` + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: + +## FAQ + +> What is the difference between `onModeEnter` and `route.init` + +`onModeEnter` gets run first than `route.init`; however, each route can have +their own `init`, but they share the `onModeEnter`. + +> How can I change the `workList` appearance or add a new login page? + +This is where `OHIF-v3` shines! Since the default `layoutTemplate` is written +for the viewer part, you can simply add a new `layoutTemplate` and use the +component you have written for that route. `Mode` handle showing the correct +component for the specified route. + +```js +function modeFactory() { + return { + id: 'viewer', + displayName: '', + routes: [ + { + path: 'worklist', + init, + layoutTemplate: ({ location, servicesManager }) => { + return { + id: 'worklistLayout', + props: { + component: 'myNewWorkList', + }, + }; + }, + }, + ], + /* + ... + */ + }; +} +``` + +> How can I navigate to (or show) a different study via the browser history/URL? + +There is a command that does this: `navigateHistory`. It takes an object +argument with the `NavigateHistory` type: + +``` +export type NavigateHistory = { + to: string; // the URL to navigate to + options?: { + replace?: boolean; // replace or add/push to history? + }; +}; +``` + +For instance one could bind a hot key to this command to show a specific study +like this... + +``` + { + commandName: 'navigateHistory', + commandOptions: { + to: + '/viewer?StudyInstanceUIDs=1.2.3', + }, + context: 'DEFAULT', + label: 'Nav Study', + keys: ['n'], + isEditable: true, + }, +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/modes/validity.md b/platform/docs/versioned_docs/version-3.8/platform/modes/validity.md new file mode 100644 index 000000000..e3f70af76 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/modes/validity.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 4 +sidebar_label: Validity +--- +# Mode: Validity + + +## Overview +There are two mechanism for checking the validity of a mode for a study. + +- `isValidMode`: which is called on a selected study in the workList. +- `validTags` + + + +## isValidMode +This hook can be used to define a function that return a `boolean` which decided the +validity of the mode based on `StudyInstanceUID` and `modalities` that are in the study. + +For instance, for pet-ct mode, both `PT` and 'CT' modalities should be available inside the study. + +```js +function modeFactory() { + return { + id: '', + displayName: '', + isValidMode: ({ modalities, StudyInstanceUID }) => { + const modalities_list = modalities.split('\\'); + const validMode = ['CT', 'PT'].every(modality => modalities_list.includes(modality)); + return validMode; + }, + /* + ... + */ + } +} +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/pwa-vs-packaged.md b/platform/docs/versioned_docs/version-3.8/platform/pwa-vs-packaged.md new file mode 100644 index 000000000..bdc411e6c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/pwa-vs-packaged.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 3 +--- + +# PWA vs Packaged + +It's important to know that the OHIF Viewer project provides two different build +processes: + +```bash +# Static Asset output: For deploying PWAs +yarn run build +``` + +## Progressive Web Application (PWA) + +> [Progressive Web Apps][pwa] are a new breed of web applications that meet the +> [following requirements][pwa-checklist]. Notably, targeting a PWA allows us +> provide a reliable, fast, and engaging experience across different devices and +> network conditions. + +The OHIF Viewer is maintained as a [monorepo][monorepo]. We use WebPack to build +the many small static assets that comprise our application. Also generated is an +`index.html` that will serve as an entry point for loading configuration and the +application, as well as a `service-worker` that can intelligently cache files so +that subsequent requests are from the local file system instead of over the +network. + +You can read more about this particular strategy in our +[Build for Production Deployment Guide](./../deployment/build-for-production.md) + +## Commonjs Bundle (Packaged Script) + +We are not supporting `Commonjs` bundling inside `OHIF-v3`. diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/faq/scope-of-project.md b/platform/docs/versioned_docs/version-3.8/platform/scope-of-project.md similarity index 95% rename from platform/docs/versioned_docs/version-2.0-deprecated/faq/scope-of-project.md rename to platform/docs/versioned_docs/version-3.8/platform/scope-of-project.md index b61f73d80..4ac68daa6 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/faq/scope-of-project.md +++ b/platform/docs/versioned_docs/version-3.8/platform/scope-of-project.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 1 --- # Scope of Project @@ -21,7 +21,7 @@ data source. To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files. These can be delivered to your end users however you would like: -- From the local networok +- From the local network - From a remote web server - From a CDN (content delivery network) - From a service-worker's cache @@ -38,7 +38,7 @@ data; only the configuration necessary to interface with one or more of these many data sources. The OHIF Viewer's scope **DOES** include configuration and support for services that are protected with OpenID-Connect. -In an effort to aide our users and contributors, we attempt to provide several +In an effort to aid our users and contributors, we attempt to provide several [deployment and hosting recipes](./deployment/index.md) as potential starting points. These are not meant to be rock solid, production ready, solutions; like most recipes, they should be augmented to best fit you and your organization's @@ -55,7 +55,7 @@ the OHIF Viewer as a desktop application. _Does the OHIF Viewer work with the local filesystem?_ -It is possible to accomplish this through extensions; however, for an user +It is possible to accomplish this through extensions; however, for a user experience that accommodates a large number of studies, you would likely need to package the OHIF Viewer as an [Electron app][electron]. diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/services/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/services/_category_.json similarity index 61% rename from platform/docs/versioned_docs/version-2.0-deprecated/services/_category_.json rename to platform/docs/versioned_docs/version-3.8/platform/services/_category_.json index 48e92f47c..9a6c3ae5f 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/services/_category_.json +++ b/platform/docs/versioned_docs/version-3.8/platform/services/_category_.json @@ -1,4 +1,4 @@ { "label": "Services", - "position": 7 + "position": 11 } diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/DicomMetadataStore.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/DicomMetadataStore.md new file mode 100644 index 000000000..927b8b8e5 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/DicomMetadataStore.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +sidebar_label: DICOM Metadata Store +--- +# DICOM Metadata Store + + +## Overview +`DicomMetaDataStore` is the central location that stores the metadata in `OHIF-v3`. There +are several APIs to add study/series/instance metadata and also for getting from the store. +DataSource utilize the `DicomMetaDataStore` to add the retrieved metadata to `OHIF Viewer`. + +> In `OHIF-v3` we have significantly changed the architecture of the metadata storage to +> provide a much cleaner way of handling metadata-related tasks and services. Classes such as +> `OHIFInstanceMetadata`, `OHIFSeriesMetadata` and `OHIFStudyMetadata` has been removed and +> replaced with `DicomMetaDataStore`. +> + + +## Events +There are two events that get publish in `DicomMetaDataStore`: + + +| Event | Description | +|-----------------|------------------------------------------------------------------------------------------------| +| SERIES_ADDED | Fires when all series of one study have added their summary metadata to the `DicomMetaDataStore` | +| INSTANCES_ADDED | Fires when all instances of one series have added their metadata to the `DicomMetaDataStore` | + + +## API +Let's find out about the public API for `DicomMetaDataStore` service. + +- `EVENTS`: Object including the events mentioned above. You can subscribe to these events + by calling DicomMetaDataStore.subscribe(EVENTS.SERIES_ADDED, myFunction). [Read more about pub/sub pattern here](../pubsub.md) + +- `addInstances(instances, madeInClient = false)`: adds the instances' metadata to the store. madeInClient is explained in detail below. After + adding to the store it fires the EVENTS.INSTANCES_ADDED. + +- `addSeriesMetadata(seriesSummaryMetadata, madeInClient = false)`: adds series summary metadata. After adding it fires EVENTS.SERIES_ADDED + +- `addStudy(study)`: creates and add study-level metadata to the store. + +- `getStudy(StudyInstanceUID)`: returns the study metadata from the store. It includes all the series and instance metadata in the requested study + +- `getSeries(StudyInstanceUID, SeriesInstanceUID`: returns the series-level metadata for the requested study and series UIDs. + +- `getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)`: returns the instance metadata based on the study, series and sop instanceUIDs. + +- `geteInstanceFromImageId`: returns the instance metadata based on the requested imageId. It searches the store for the instance that has the same imageId. + + + +### madeInClient + +Since upon adding the metadata to the store, the relevant events are fired, and there are +other services that are subscribed to these events (`HangingProtocolService` or `DisplaySetService`), sometimes +we want to add instance metadata but don't want the events to get fired. For instance, when +you are caching the data for the next study in advance, you probably don't want to trigger hanging protocol +logic, so you set `madeInClient=true` to not fire events. + + +## Storage +As discussed before, there are several API that enables getting metadata from the store and adding to the store. +However, it is good to have an understanding of where they get +stored and in what format and hierarchy. `_model` is a private variable in the store +which holds all the metadata for all studies, series, and instances, and it looks like: + + +```js title="platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js" +const _model = { + studies: [ + { + /** Study Metadata **/ + seriesLists: [ + { + // Series in study from dicom web server 1 (or different backend 1) + series: [ + { + // Series 1 Metadata + instances: [ + { + // Instance 1 metadata of Series 1 + }, + { + // Instance 2 metadata of Series 1 + }, + /** Other instances metadata **/ + ], + }, + { + // Series 2 Metadata + instances: [ + { + // Instance 1 metadata of Series 2 + }, + { + // Instance 2 metadata of Series 1 + }, + /** Other instances metadata **/ + ], + }, + ], + }, + { + // Series in study from dicom web server 2 (or different backend 2) + /** ... **/ + }, + ], + }, + ], +} +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/DisplaySetService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/DisplaySetService.md new file mode 100644 index 000000000..8b8bcae67 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/DisplaySetService.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 3 +sidebar_label: DisplaySet Service +--- +# DisplaySet Service + + +## Overview +`DisplaySetService` handles converting the `instanceMetadata` into `DisplaySet` that `OHIF` uses for the visualization. `DisplaySetService` gets initialized at service startup time, but is then cleared in the `Mode.jsx`. During the initialization `SOPClassHandlerIds` of the `modes` gets registered with the `DisplaySetService`. + +:::tip + +DisplaySet is a general set of entities and contains links to bunch of displayable objects (images, etc.) Some series might get split up into different displaySets e.g., MG might have mixed views in a single series, but users might want to have separate LCC, RCC, etc. for hanging protocol usage. A viewport renders a display set into a displayable object. + +imageSet is a particular implementation of image displays. +::: + + +> Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series. Note +that this is an ordered operation, and consumes the instances as it proceeds, with the first registered +handlers being able to consume instances first. + +DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md). They are ALSO updated when +the DicommetaDataStore receives new data. This update first checks the addInstances +of existing `DisplaySet` values to see if the new instance belongs in an existing set. +Then, the same process is used as was originally done to create new display sets. + +NOTE: Any instances not matched are NOT added to any display set and will not be displayed. + +## Adding `madeInClient` display sets +It is possible to filter or combine display sets from different series by +performing the filter operation desired, and then calling the `addActiveDisplaySets` +on the new `DisplaySet` instances. This allows operations like combining +two series or sub-selecting a series. + +## Events +There are three events that get broadcasted in `DisplaySetService`: + +| Event | Description | +| -------------------- | ---------------------------------------------------- | +| DISPLAY_SETS_ADDED | Fires a displayset is added to the displaysets cache | +| DISPLAY_SETS_CHANGED | Fires when a displayset is changed | +| DISPLAY_SETS_REMOVED | Fires when a displayset is removed | +| DISPLAY_SET_SERIES_METADATA_INVALIDATED | Fires when a displayset's series metadata has been altered. An object payload for the event is sent with properties: `displaySetInstanceUID` - the UID of the display set affected; `invalidateData` - boolean indicating if data should be invalidated + + +## API +Let's find out about the public API for `DisplaySetService`. + +- `EVENTS`: Object including the events mentioned above. You can subscribe to these events + by calling DisplaySetService.subscribe(EVENTS.DISPLAY_SETS_CHANGED, myFunction). [Read more about pub/sub pattern here](../pubsub.md) + +- `makeDisplaySets(input, { batch, madeInClient, settings } = {}`): Creates displaySet for the provided + array of instances metadata. Each display set gets a random UID assigned. + + - `input`: Array of instances Metadata + - `batch = false`: If you need to pass array of arrays of instances metadata to have a batch creation + - `madeInClient = false`: Disables the events firing + - `settings = {}`: Hanging protocol viewport or rendering settings. For instance, setting the initial `voi`, or activating a tool upon + displaySet rendering. [Read more about hanging protocols settings here](./HangingProtocolService.md#Settings) + + +- `getDisplaySetByUID`: Returns the displaySet based on the DisplaySetUID. + +- `getDisplaySetForSOPInstanceUID`: Returns the displaySet that includes an image with the provided SOPInstanceUID + +- `getActiveDisplaySets`: Returns the active displaySets + +- `deleteDisplaySet`: Deletes the displaySets from the displaySets cache + +- `addActiveDisplaySets`: Adds a new display set independently of the make operation. + +- `setDisplaySetMetadataInvalidated`: Fires the `DISPLAY_SET_SERIES_METADATA_INVALIDATED` event. diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/HangingProtocolService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/HangingProtocolService.md new file mode 100644 index 000000000..de33e9bbd --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/HangingProtocolService.md @@ -0,0 +1,367 @@ +--- +sidebar_position: 4 +sidebar_label: Hanging Protocol Service +--- + +# Hanging Protocol Service + +## Overview + + +This service handles the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The +winning protocol (highest score) gets applied and its settings run for the viewports +to be arranged. + +You can read more about a HangingProtocol Structure and its properties in the +[HangingProtocol Module](../../extensions/modules/hpModule.md). + +The rest of this page is dedicated on how the Hanging Protocol Service works and +what you can do with it. + +## Protocols + +Protocols are provided by each extension's HangingProtocolModule and are +registered automatically to the HangingProtocolService. + +All protocols are stored in the `HangingProtocolService` using their `id` as the key, and the protocol itself as the value. + +## Protocol Definition +Protocols are defined in a getHangingProtocolModule inside an extension. As such, +they are defined with a module structure that starts with an id, and has field protocol +that is the actual protocol definition. This setup allows defining more than +one protocol within a module, each one needing it's own definition file. + +```javascript +import MyProtocol from './MyProtocol'; +export default function getHangingProtocolModule() { + return [ + { + id: MyProtocol.id, + protocol: MyProtocol, + }, + ]; +} +``` + +Within the protocol itself, the structure is laid out as described in the HangingProtocol.ts +type definition, starting with `Protocol`. See the type definition for more details. + +## Events + +There are two events that get publish in `HangingProtocolService`: + +| Event | Description | +| ------------ | -------------------------------------------------------------------- | +| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` | +| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols, or when the applied stage is changed. | +| RESTORE_PROTOCOL | Fires when the protocol or stage is restored, for example, after turning off MPR mode | +| STAGE_ACTIVATION | Fires when the stages are known to have stage.status set. | + +## Stage Activation and Status +Sometimes a hanging protocol can be applicable generally, but not all stages +should be shown by default, or should be shown at all. This can be handled by +using the stage activation to control whether the stage is shown by default (`enabled`), +whether it can be navigated to (`passive`) or whether it should not be shown +at all (`disabled`). + +The `stage.status` is used to control this, and the status is controlled by +the stage activate. The status values are: + +* enabled - meaning that the stage is fully applicable +* passive - meaning that the stage can be applied, but might be missing details +* disabled - meaning that the study has insufficient information for this stage + +The default values for no `stageActivation` are to assume that `enabled` has `minViewports` of 1, +and `passive` has `minViewports=0`. That is, enable the stage if at least one +viewport is filled, and make it passive if no viewports are filled. + +The setting for these are controlled by the stageActivation property, for example +the following: + +```javascript +stageActivation: { + // The enabled activation specifies requirements to enable the stage, that is, + // make it preferred. + enabled: { + // The default value here is 1, and indicates how many non-blank viewports + // are required. + minViewportsMatched: 3, + // This enables specifying cross cutting concerns, such as having a stage + // only apply to males or females, and is a list of display set selector ids + displaySetSelectorsMatched: ['dsMale'], + }, + // The passive check is performed first. If it fails, the enabled is NOT + // checked, but the status set to disabled. The default passive check + // should always be passed, so it is fine to just define enabled if desired. + passive: { + // The default is 0, which means allow the stage even if no viewports are + // filled. This allows dragging and dropping into the viewports to + // make matches manually, which can then be re-used for other stages. + minViewportsMatched: 0, + displaySetSelectorsMatched: [...], + }, +} +``` + +## API + +- `destroy`: Destroys the HP service + +- `reset` and `onModeEnter`: Resets the HP service to not have any active + hanging protocols + +- `getActiveProtocol`: Returns an object of the internal state of the HP service, + useful for storing said state, as well as for getting direct access to the + protocol and stage objects. Users of this should count on it being not completely + stable as to exactly what this returns, as internal details can change. + +- `getState`: Returns the currently applied protocol ID, stage index and active study UID. + This information is storable/usable as state information to be used elsewhere. + +- `getDefaultProtocol`: Returns the default protocol to apply. + +- `getMatchDetails`: returns an object which contains the details of the + matching for the viewports, displaySets and whether the protocol is + applied to the viewport or not yet. This is deprecated as it is expected + to be communicated by events instead. + +- `getProtocols`: Returns a list of the currently active protocols. + +- `getProtocolById`: Gets the protocol with the given id. + +- `addProtocol`: adds provided protocol to the list of registered protocols + for matching. Will replacing any protocol with the same id, allowing, for example, + to replace the default protocol. + +- `setActiveProtocols`: Choose the protocols which are active. Can take a +single protocol id or a list. When a single one is provided, that one will be +applied whether or not the required rules match. Called automatically on mode +init. + +- `setActiveStudyUID`: Sets the given study UID as active, which has significance + in terms of the matching rules being able to match against the active study. + +- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided + studyMetaData and optional protocolId. If protocol is not given, HP Matching + engine will search all the registered protocols for the best matching one + based on the constraints. + +- `registerImageLoadStrategy`: Adds a custom image load strategy. + +- `addCustomAttribute`: adding a custom attribute for matching. (see below) + +- `setProtocol`: applies a protocol to the current studies, it can be used for instance to apply a + hanging protocol when pressing a button in the toolbar. We use this for applying 'mpr' + hanging protocol when pressing the MPR button in the toolbar. `setProtocol` will + accept a set of options that can be used to define the displaySets that will be + used for the protocol. If no options are provided, all displaySets will + be used to match the protocol. + +- `getStageIndex`: Finds the stage index for a given set of match keys. Currently + only works on the currently active protocol, but is supposed to be able to work + with other protocols as well. + +- `getMissingViewport`: Returns a viewport object to be used as the missing + viewport instance. This is used to fill out new viewports. + +Default initialization of the modes handles running the `HangingProtocolService` + +## Hanging Protocol Instance Definition +A hanging protocol has an id provided in the module which is used to identify +the protocol. Mostly these should include the module name so that they +do not overlap, with the suggested id being `${moduleId}.${simpleName}`. The +'default' name is used as the hanging protocol id when no other protocol applies, +and can be set as the last module listed containing 'default'. + +A hanging protocol can also be defined with a generator. +A generator is a function we can write this way: + +```ts +function protocolGenerator({ servicesManager, commandsManager }) { + // Some computations using services and commands ... + + return { + protocol: generatedProtocol + } +} +``` + +See the typescript definitions for more details on the structure of protocols. + +## Additional viewports for layout - `defaultViewport` +Sometimes the user manually selects a layout of a given size, say `2x3`. The +hanging protocol can define what viewport options to use for this viewport by +defining an extra viewport option in `defaultViewport`. For example: + +```javascript + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, +``` + +This allows defining the type of additional viewports, what tool group etc they +are allowed in, and which display set is used to fill them. In the above case, +the display set is the same as the other viewports, but the +`matchedDisplaySetsIndex=-1`, so that means find the next matching display set +from the display set selector which isn't already filling a view. + +## Custom Attribute +In some situations, you might want to match based on a custom +attribute and not the DICOM tags. For instance, +if you have assigned a `timepointId` to each study, and you want to match based on it. +Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching. + +There are various ways that you can let `HangingProtocolService` know of you +custom attribute. We will show how to add it inside the mode configuration. + +```js +const defaultProtocol = { + id: 'defaultProtocol', + /** ... **/ + protocolMatchingRules: [ + { + weight: 3, + attribute: 'timepoint', + constraint: { + equals: 'first', + }, + required: false, + }, + ], + displaySetSelectors: { + /** ... */ + } + stages: [ + /** ... **/ + ], + numberOfPriorsReferenced: -1, +}; + +// Custom function for custom attribute +const getTimePointUID = metaData => { + // requesting the timePoint Id + return myBackEndAPI(metaData); +}; + +function modeFactory() { + return { + id: 'myMode', + /** .. **/ + routes: [ + { + path: 'myModeRoute', + init: async ({}) => { + const { + DicomMetadataStore, + HangingProtocolService, + } = servicesManager.services; + + const onSeriesAdded = ({ + StudyInstanceUID, + madeInClient = false, + }) => { + const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); + + // Adding custom attribute to the hangingprotocol + HangingProtocolService.addCustomAttribute( + 'timepoint', + 'timepoint', + metaData => getFirstMeasurementSeriesInstanceUID(metaData) + ); + + HangingProtocolService.run(studyMetadata); + }; + + DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + onSeriesAdded + ); + }, + }, + ], + /** ... **/ + }; +} +``` + +### Custom Attributes for Viewport Options + +The custom attributes can also be used for viewport options. This example, +from the default hanging protocol navigates the image to the image +specified in the URL: + +```javascript +viewportOptions: { + initialImageOptions: { + // custom attribute name is selected by 'custom' + custom: 'sopInstanceLocation', + // This is the value returned if the above doesn't return anything + defaultValue: { index: 5 }, + } +} +``` + +### Included Custom Attributes + +A few custom attributes are included under @ohif/extension-test, these are namely: +*sameAs +*maxNumImageFrames +*numberOfDisplaySets + +To use these included custom attributes, the extension will need to be enabled under platform/app/pluginConfig.json: + +```javascript +{ + "extensions": [ + ... + { + "packageName": "@ohif/extension-test", + "version": "3.4.0" + }, + ... + ] +} + ``` + +Furthermore, the extension will also need to be included under extensionDependencies in the desired mode (e.g. modes/tmtv/src/index.js): + +```javascript +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-tmtv': '^3.0.0', + '@ohif/extension-test': '^0.0.1', + }; + ``` + +The below example modifies the included hanging protocol (extensions/tmtv/src/getHangingProtocolModule.js) and uses the sameAs attribute included in the @ohif/extension-test extension to check that the selected PT has the same frame of reference as the CT: + +```javascript +ptDisplaySet: { + ... + seriesMatchingRules: [ + { + attribute: 'sameAs', + sameAttribute: 'FrameOfReferenceUID', + sameDisplaySetId: 'ctDisplaySet', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ... +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/MeasurementService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/MeasurementService.md new file mode 100644 index 000000000..f5ede2f14 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/MeasurementService.md @@ -0,0 +1,182 @@ +--- +sidebar_position: 6 +sidebar_label: Measurement Service +--- + +# Measurement Service + +## Overview + +`MeasurementService` handles the internal measurement representation inside +`OHIF` platform. Developers can add their custom `sources` with `mappers` to +enable adding measurements inside OHIF. Currently, we are maintaining +`CornerstoneTools` annotations and corresponding mappers can be found inside the +`cornerstone` extension. However, `MeasurementService` can be configured to work +with any custom tools given that its `mappers` is added to the +`MeasurementService`. We can see the overall architecture of the +`MeasurementService` below: + +![services-measurements](../../../assets/img/services-measurements.png) + +## Events + +There are seven events that get publish in `MeasurementService`: + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| MEASUREMENT_UPDATED | Fires when a measurement is updated | +| MEASUREMENT_ADDED | Fires when a new measurement is added | +| RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) | +| MEASUREMENT_REMOVED | Fires when a measurement is removed | +| MEASUREMENTS_CLEARED | Fires when all measurements are deleted | +| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. | +| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. | + +## API + +- `getMeasurements`: returns array of measurements + +- `getMeasurement(id)`: returns the corresponding measurement based on the + provided Id. + +- `remove(id, source)`: removes a measurement and broadcasts the + `MEASUREMENT_REMOVED` event. + +- `clearMeasurements`: removes all measurements and broadcasts + `MEASUREMENTS_CLEARED` event. + +- `createSource(name, version)`: creates a new measurement source, generates a + uid and adds it to the `sources` property of the service. + +- `addMapping(source, definition, matchingCriteria, toSourceSchema, toMeasurementSchema)`: + adds a new measurement matching criteria along with mapping functions. We will + learn more about [source/mappers below](#source--mappers) + +- `update`: updates the measurement details and fires `MEASUREMENT_UPDATED` + +- `addRawMeasurement(source,definition,data,toMeasurementSchema,dataSource = {}` + : adds a raw measurement into a source so that it may be converted to/from + annotation in the same way. E.g. import serialized data of the same form as + the measurement source. Fires `MEASUREMENT_UPDATED` or `MEASUREMENT_ADDED`. + Note that, `MeasurementService` handles finding the correct mapper upon new + measurements; however, `addRawMeasurement` provides more flexibility. You can + take a look into its usage in `dicom-sr` extension. + + - `source`: The measurement source instance. + - `definition`: The source definition you want to add the measurement to. + - `data`: The data you wish to add to the source. + - `toMeasurementSchema`: A function to get the `data` into the same shape as + the source definition. + +- `jumpToMeasurement(viewportId, id)`: calls the listeners who have + subscribed to `JUMP_TO_MEASUREMENT`. + +## Source / Mappers + +To create a custom measurement source and relevant mappers for each tool, you +can take a look at the `init.js` inside the `cornerstone` extension. In which we +are registering our `CornerstoneTools-v4` measurement source to +MeasurementService. Let's take a peek at the _simplified_ implementation +together. To achieve this, for each tool, we need to provide three mappers: + +- `matchingCriteria`: criteria used for finding the correct mapper for the drawn + tool. +- `toAnnotation`: tbd +- `toMeasurement`: a function that converts the tool data to OHIF internal + representation of measurement data. + +```js title="extensions/cornerstone/src/utils/measurementServiceMappings/Length.js" +function toMeasurement( + csToolsAnnotation, + DisplaySetService, + getValueTypeFromToolType +) { + const { element, measurementData } = csToolsAnnotation; + + /** ... **/ + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + + /** ... **/ + return { + id: measurementData.id, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: measurementData.label, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; +} + +////////////////////////////////////////// + +// extensions/cornerstone/src/init.js + +const Length = { + toAnnotation, + toMeasurement, + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], +}; + +const _initMeasurementService = (MeasurementService, DisplaySetService) => { + /** ... **/ + + const csToolsVer4MeasurementSource = MeasurementService.createSource( + 'CornerstoneTools', + '4' + ); + + /* Mappings */ + MeasurementService.addMapping( + csToolsVer4MeasurementSource, + 'Length', + Length.matchingCriteria, + toAnnotation, + toMeasurement + ); + + /** Other tools **/ + return csToolsVer4MeasurementSource; +}; +``` + + +## Auto complete +Use a customization service to add more customizations for measurement labels. Later, when adding a measurement, the user will be prompted to choose from a list of labels. + +```js +customizationService.addModeCustomizations([ + { + id: 'measurementLabels', + labelOnMeasure: true, + exclusive: true, + items: [ + { value: 'Head', label: 'Head' }, + { value: 'Neck', label: 'Neck' }, + { value: 'Knee', label: 'Knee' }, + { value: 'Toe', label: 'Toe' }, + ], + }, +]); +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/PanelService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/PanelService.md new file mode 100644 index 000000000..b0a530b85 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/PanelService.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 8 +sidebar_label: Panel Service +--- + +# Panel Service + +## Overview + +The Panel Service provides for activating/showing a panel that was registered +via the `getPanelModule` extension method. Such panels can be either explicitly +activated or implicitly triggered to activate when some other event occurs. + +## Events + +The following events are published in `PanelService`. + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| ACTIVATE__PANEL | Fires a `ActivatePanelEvent` when a particular panel should be activated (i.e. shown). | + + +## API + +### Panel Activation + +- `activatePanel`: Fires the `ACTIVATE_PANEL` event for a particular panel (id). +An optional `forceActive` flag can be passed that when `true` "forces" a +panel to show. Ultimately, it is up to a panel's container whether it +is appropriate to activate/show the panel. For instance, if the user opened and then +closed a side panel that contains the panel to activate, that side panel +may decide that the user knows best and will not open the panel (again). + +- `addActivatePanelTriggers`: Creates and returns event subscriptions that when +fired will activate the specified panel with an optional `forceActive` flag +(see `activatePanel`). This allows for panel activation to be directly triggered +by some other event(s). When the triggers are no longer needed, simply +unsubscribe to the returned subscriptions. For example, a panel +for tracking measurements might get activated every time the +`MeasurementService` fires a `MEASUREMENT_ADDED` event like this: + ```js + panelService.addActivatePanelTriggers('measurement-tracking-panel-id', [ + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + ]); + ``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/SegmentationService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/SegmentationService.md new file mode 100644 index 000000000..bd99e12de --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/SegmentationService.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 7 +sidebar_label: Segmentation Service +--- + +# Segmentation Service + +## Overview + +Using Segmentation Service you can create, edit and delete segmentation data, and +change appearance of the segmentation including color, opacity and visibility. + +Segmentations in OHIF are based on the Segmentations in Cornerstone3D. You can +read more about it in the [Cornerstone Segmentation](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/segmentation/). OHIF currently only supports +one representation of the segmentation data. + +## Events + +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_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 | + + +## API + +### Segmentation Creation + +- `createSegmentationForDisplaySet`: 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. + + +### Segmentation Behavior + +- setActiveSegmentationForToolGroup, getSegmentations, getSegmentation, jumpToSegmentCenter, highlightSegment + + +### Segment Behavior + +- setSegmentLocked, removeSegment, addSegment, setSegmentLocked, setSegmentLabel, setActiveSegment, +setSegmentRGBAColor + +### Segmentation Configuration + +Setters + +- setSegmentVisibility, setSegmentColor, setSegmentRGBA, setSegmentOpacity, toggleSegmentationVisibility, diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/StateSyncService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/StateSyncService.md new file mode 100644 index 000000000..ebf056526 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/StateSyncService.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 8 +sidebar_label: State Sync +--- + +# State Sync + +## Overview + +Applications often need a way to save and restore state, allowing users to continue their work seamlessly across sessions or after restarts. This is essential for keeping user preferences, preserving app context, and providing a consistent experience. For example, hanging protocol layouts, and window level improves user interactions, allowing for smooth navigation between protocols and restoring layouts upon returning. This document explains how to manage and sync state in the application using Zustand stores, providing built-in state stores and tools for extensions to create and manage their own. + + +## Built-in Zustand Stores + +The following stores are available for managing different aspects of application state: + +* **`useLutPresentationStore`**: Manages LUT (window level) presentation state. Key features: + * `setLutPresentation(key, value)`: Sets a LUT presentation for a given `key`. + * `clearLutPresentationStore()`: Clears all stored LUT presentations. + * `getPresentationId(id, options)`: Retrieves the presentation ID based on viewport and display set information. + +* **`usePositionPresentationStore`**: Manages viewport position (camera, initial image) state. Key features: + * `setPositionPresentation(key, value)`: Sets a position presentation for a given `key`. + * `clearPositionPresentationStore()`: Clears all stored position presentations. + * `getPresentationId(id, options)`: Retrieves the presentation ID based on viewport and display set information. + * `getPositionPresentationId(viewport, viewports?, isUpdatingSameViewport?)`: Gets the position presentation ID. + +* **`useSegmentationPresentationStore`**: Manages segmentation presentation state. Key features: + * `setSegmentationPresentation(presentationId, value)`: Sets a segmentation presentation for a given `presentationId`. + * `clearSegmentationPresentationStore()`: Clears all stored segmentation presentations. + * `getPresentationId(id, options)`: Retrieves the presentation ID based on viewport, display set, and services manager information. + * `addSegmentationPresentation(presentationId, segmentationPresentation, { servicesManager })`: Adds a new segmentation presentation. + * `getSegmentationPresentationId({ viewport, servicesManager })`: Retrieves the current segmentation presentation ID. + +* **`useSynchronizersStore`**: Manages viewport synchronization state. Key features: + * `setSynchronizers(viewportId, synchronizers)`: Sets synchronizers for a specific viewport. + * `clearSynchronizersStore()`: Clears the entire synchronizers store. + + +## Creating Custom State Stores with Zustand + +Extensions can create their own Zustand stores to manage custom state. This approach leverages Zustand's simplicity and performance. + +```javascript +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +// Define your state shape +interface MyCustomState { + count: number; + increment: () => void; + reset: () => void; +} + +// Create your store +const useMyCustomStore = create()( + devtools((set) => ({ + count: 0, + increment: () => set((state) => ({ count: state.count + 1 })), + reset: () => set({ count: 0 }), + }), { name: 'MyCustomStore' }) // Use devtools for debugging +); + +// Use the store in your component +function MyComponent() { + const { count, increment, reset } = useMyCustomStore(); + + return ( +
+

Count: {count}

+ + +
+ ); +} +``` + +### Best Practices + +* **Naming:** Use a clear and descriptive name for your store, prefixed with `use`, e.g., `useMyCustomStore`. +* **Typing:** Define the state interface using TypeScript for better type safety and code maintainability. +* **Devtools:** Use the `devtools` middleware in development to easily inspect and debug your store's state changes. +* **State Updates:** Use the `set` function to update the state, ensuring immutability by returning a new state object or using the callback form to access the previous state. + + + +## Migration from Legacy State Sync Service + +Existing extensions using the legacy `stateSyncService` should migrate to using individual Zustand stores. This involves: + +1. Creating a new Zustand store for each piece of state previously managed by `stateSyncService`. +2. Updating the extension code to use the new store instead of `stateSyncService`. +3. Removing the legacy `stateSyncService` registration code. diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/SyncGroupService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/SyncGroupService.md new file mode 100644 index 000000000..835fd5621 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/SyncGroupService.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 8 +sidebar_label: SyncGroup Service +--- + +# Sync Group Service + +## Overview + +The `SyncGroupService` is responsible for managing synchronization groups in the OHIF Viewer. Synchronization groups allow multiple viewports to be synchronized based on various criteria, such as camera position, window level, zoom/pan, and image slice position. This service provides a centralized way to create, update, and manage synchronization groups. + +Right now, synchronization groups can be defined in the hanging protocols or manually assigning buttons. + + + + +## API + +- `getSyncCreatorForType(type)`: Returns the synchronizer creator function for the specified type. +- `addSynchronizerType(type, creator)`: Adds a new synchronizer type with a custom creator function. +- `getSynchronizer(id)`: Retrieves a synchronizer by its ID. +- `getSynchronizersOfType(type)`: Retrieves an array of synchronizers of the specified type. +- `addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups)`: Adds a viewport to one or more synchronization groups. +- `destroy()`: Destroys all synchronizers. +- `getSynchronizersForViewport(viewportId)`: Retrieves an array of synchronizers associated with the specified viewport. +- `removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroupId?)`: Removes a viewport from a specific synchronization group or all synchronization groups if no group ID is provided. + +## Usage +### Via hanging protocols +You can set up different types of synchronization groups for your viewports. For example, in the TMTV hanging protocol (`extensions/tmtv/src/getHangingProtocolModule.js`), we can see how different synchronization groups are defined for various viewports: + +```javascript +const ptAXIAL = { + viewportOptions: { + // ... + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncInvertState: false, + }, + }, + ], + }, + // ... +}; +``` + + + +In this example, the `ptAXIAL` viewport is part of three synchronization groups: + +1. `cameraPosition` group with the ID `'axialSync'`: This group synchronizes the camera position across viewports that are both source and target. +2. `voi` (Window Level) group with the ID `'ptWLSync'`: This group synchronizes the window level settings across viewports that are both source and target. +3. `voi` group with the ID `'ptFusionWLSync'`: This group synchronizes the window level settings, but the `ptAXIAL` viewport is only a source, not a target. + + +:::tip +You can control the state of the synchronizer via a toolbar button after you define the synchronization group in the hanging protocol. + +```js +{ + id: 'SyncToggle', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-info', + label: 'toggle', + commands: { + commandName: 'toggleSynchronizer', + commandOptions: { + syncId: 'axialSync' + } + } + }, +}, +``` + +as you can see by using the `toggleSynchronizer` command you can toggle the state of the synchronizer for the specified syncId. + +::: + +### Manually through a button +You can create a button on the toolbar that you provice the synchronization group type, +and it applys it to all viewports. + +:::note +Currently we don't have a proper way to select viewports to apply the synchronization group to. It is applied to all applicable viewports +::: + +For instance look at `imageSliceSync` button in the longitudinal mode (`modes/longitudinal/src/moreTools.ts`) and how it runs a command + +```js +ToolbarService.createButton({ + id: 'ImageSliceSync', + icon: 'link', + label: 'Image Slice Sync', + tooltip: 'Enable position synchronization on stack viewports', + commands: [ + { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'imageSlice', + }, + }, + ], +}) +``` + +You can create another button to toggle 'voi' synchronization. Currently we group +viewports by modality and apply the voi synchronization to all viewports of the same modality. + +```js +ToolbarService.createButton({ + id: 'VoiSync', + icon: 'link', + label: 'VOI Sync', + tooltip: 'Enable VOI synchronization on viewports', + commands: [ + { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'voi', + }, + }, + ], +}) +``` + +:::tip +For your custom synchronization groups, you can create a new synchronizer type and follow the +same pattern as the existing synchronizers. +::: diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/ToolGroupService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/ToolGroupService.md new file mode 100644 index 000000000..0d7a04cf0 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/ToolGroupService.md @@ -0,0 +1,89 @@ +--- +sidebar_position: 7 +sidebar_label: ToolGroup Service +--- + +# Tool Group Service + +## Overview + +The `ToolGroupService` is responsible for managing tool groups in the OHIF Viewer. + +:::tip +Read more about toolGroups [here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolGroups) +::: + +It allows you to create, update, and manage tool groups and the tools associated with them. Tool groups are used to organize and control the behavior of various tools in the viewer, such as window level, pan, zoom, measurements, and annotations. + +## Events + +The `ToolGroupService` emits the following events: + +| Event | Description | +| ---------------------------------- | ----------------------------------------------- | +| `VIEWPORT_ADDED` | Fires when a viewport is added to a tool group | +| `TOOLGROUP_CREATED` | Fires when a new tool group is created | + +## API + +- `getToolGroup(toolGroupId?)`: Retrieves a tool group by its ID. If no ID is provided, it returns the tool group for the active viewport. +- `getToolGroupIds()`: Returns an array of all tool group IDs. +- `getToolGroupForViewport(viewportId)`: Returns the tool group associated with the specified viewport. +- `getActiveToolForViewport(viewportId)`: Returns the active tool for the specified viewport. +- `destroy()`: Destroys all tool groups. +- `destroyToolGroup(toolGroupId)`: Destroys the specified tool group. +- `removeViewportFromToolGroup(viewportId, renderingEngineId, deleteToolGroupIfEmpty?)`: Removes a viewport from a tool group. If `deleteToolGroupIfEmpty` is true and the tool group becomes empty after removing the viewport, it will be destroyed. +- `addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId?)`: Adds a viewport to a tool group. If `toolGroupId` is not provided, the viewport will be added to all tool groups. +- `createToolGroup(toolGroupId)`: Creates a new tool group with the specified ID. +- `addToolsToToolGroup(toolGroupId, tools, configs?)`: Adds tools to the specified tool group with optional configurations. +- `createToolGroupAndAddTools(toolGroupId, tools)`: Creates a new tool group and adds the specified tools to it. +- `getToolConfiguration(toolGroupId, toolName)`: Retrieves the configuration for the specified tool in the given tool group. +- `setToolConfiguration(toolGroupId, toolName, config)`: Sets the configuration for the specified tool in the given tool group. + +## Usage + +Here's an example of how to create a new tool group and add tools to it in our basic viewer mode (modes/longitudinal/src/initToolGroups.js) + +```js +import { initToolGroups } from '@ohif/extension-cornerstone'; +import { ToolGroupService } from '@ohif/core'; + +const toolGroupService = new ToolGroupService(); + +// Create a new tool group +const defaultToolGroup = toolGroupService.createToolGroup('default'); + +// Define tools for the tool group +const tools = { + active: [ + { toolName: 'WindowLevel', bindings: [{ mouseButton: 1 }] }, + { toolName: 'Pan', bindings: [{ mouseButton: 2 }] }, + { toolName: 'Zoom', bindings: [{ mouseButton: 3 }] }, + ], + passive: [ + { toolName: 'Length' }, + { toolName: 'ArrowAnnotate' }, + { toolName: 'Bidirectional' }, + ], +}; + +// Add tools to the tool group +toolGroupService.addToolsToToolGroup('default', tools); +``` + +In this example, we create a new `ToolGroupService` instance and use it to create a new tool group with the ID `'default'`. We then define an object `tools` that contains the active and passive tools we want to add to the tool group. Finally, we call the `addToolsToToolGroup` method to add the tools to the newly created tool group. + +:::tip +You can begin the viewer with certain toggle tools already active. For example, if you have your 'referencelines' tool enabled, it will be active when the viewer starts, and the icon state will be correctly set to active as well. +::: + +```js +const tools = { + // the reset + // enabled + enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }], + }; +``` + + +![alt text](../../../assets/img/reference-lines-from-start.png) diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/ToolbarService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/ToolbarService.md new file mode 100644 index 000000000..3eb03ffd2 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/ToolbarService.md @@ -0,0 +1,264 @@ +--- +sidebar_position: 5 +sidebar_label: Toolbar Service +--- + +# Toolbar **Service** + +## Overview + +The `ToolBarService` is a straightforward service designed to handle the toolbar. Its main tasks include adding buttons, configuring them, and organizing button sections. When a button is clicked, it executes the designated commands. In the past, this service was more intricate, managing button states and logic. However, all that functionality has now been transferred to the `ToolBarModule` and evaluators. + + + +## Events + +| Event | Description | +| ----------------------- | ---------------------------------------------------------------------- | +| TOOL_BAR_MODIFIED | Fires when a button is added/removed to the toolbar | +| TOOL_BAR_STATE_MODIFIED | Fires when an interaction happens and ToolBarService state is modified | + +## API + +- `createButtonSection(key, buttons)` : creates a section of buttons in the toolbar with the given key and button Ids + +- `addButtons`: add the button definition to the service. + [See below for button definition](#button-definitions). + +- `setButtons`: sets the buttons defined in the service. It overrides all the + previous buttons + + + + +## Button Definitions + + +### Basic + +The simplest toolbarButtons definition has the following properties: + +![toolbarModule-zoom](../../../assets/img/toolbarModule-zoom.png) + + +```js +{ + id: 'Zoom', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-zoom', + label: 'Zoom', + "commands": [ + { + "commandName": "setToolActive", + "commandOptions": { + "toolName": "Zoom" + }, + "context": "CORNERSTONE" + } + ] + evaluate: 'evaluate.cornerstoneTool', + }, +}, +``` + +| property | description | values | +| ---------------- | ----------------------------------------------------------------- | ------------------------------------------- | +| `id` | Unique string identifier for the definition | \* | +| `icon` | A string name for an icon supported by the consuming application. | \* | +| `label` | User/display friendly to show in UI | \* | +| `commands` | (optional) The commands to run when the button is used. It include a commandName, commandOptions, and/or a context | Any command registered by a `CommandModule` | + + +### Nested (dropdown) + +You can use the `ohif.splitButton` type to build a button with extra tools in +the dropdown. + +- First you need to give your `primary` tool definition to the split button +- the `secondary` properties can be a simple arrow down (`chevron-down` icon) +- For adding the extra tools add them to the `items` list. + +You can see below how `longitudinal` mode is using the available toolbarModule +to create `MeasurementTools` nested button + +![toolbarModule-nested-buttons](../../../assets/img/toolbarModule-nested-buttons.png) + +```js title="modes/longitudinal/src/toolbarButtons.js" +{ + id: 'MeasurementTools', + uiType: 'ohif.splitButton', + props: { + groupId: 'MeasurementToolsGroupId', + // group evaluate to determine which item should move to the top + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: _createSetToolActiveCommands('Length'), + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Length', + }, + context: 'CORNERSTONE', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'SRLength', + toolGroupId: 'SRToolGroup', + }, + // we can use the setToolActive command for this from Cornerstone commandsModule + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Bidirectional', + }, + context: 'CORNERSTONE', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'SRBidirectional', + toolGroupId: 'SRToolGroup', + }, + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, +``` + +:::tip +split buttons can have a group evaluator (in the above example `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`) which can decide what happens +when the user interacts with the buttons. In the above example, we are promoting the button to the primary section if the cornerstone tool is not active in the list of buttons. + +There are other evaluators for instance `evaluate.group.promoteToPrimary` +which does not care about the cornerstone tool and promotes the button to the primary section anyway +::: + +:::tip +If you don't provide a group evaluator nothing would happen and the button will stay in the secondary section. +::: + +## Listeners +Sometimes you need a tool to listen to specific events in order to react properly. +You can add `listeners` for this purpose. We use this pattern for referencelineTools +which should set its source of reference upon active viewport change + +Currently you can subscribe to the following events: +- `ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED`: when the active viewport changes +- `ViewportGridService.EVENTS.VIEWPORTS_READY`: when the viewports are ready in the grid + + +```js + +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', + context: 'CORNERSTONE', + }, +]; + +ToolbarService.createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: [ + { + commandName: 'setToolEnabled', + commandOptions: { + toolName: 'ReferenceLines', + toggle: true, + }, + context: 'CORNERSTONE', + }, + ], + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', +}), + +``` + + + +## Button Sections +In order to organize the buttons, you can create button sections in the toolbar. And +assign buttons to each section separately. + +OHIF provides a `primary` section by default. You can add more sections as needed in your UI +and use toolbarService to create and manage them. (You can look at the toolBox implementation +which take advantage of having a dedicated section for the tools with advanced options, +we use that in the segmentation mode). + + +## Example + +For instance in `longitudinal` mode we are using the `onModeEnter` hook to +add the buttons to the toolbarService and assign them to the primary section. + +```js title="modes/longitudinal/src/index.js" +toolbarService.addButtons([...toolbarButtons, ...moreTools]); +toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'info', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', +]); +``` + +as you see we creating the button section and assigning buttons based on their Ids. + +:::tip +You can even duplicate the same button in different sections and the button will be +in sync in all sections (thanks to the evaluation system). +::: + +:::tip +we will add more section in the toolbar (other than primary) in the future. +::: + +:::note +Don't forget to set up your toolGroups to ensure that your buttons function correctly. Buttons serve as a visual interface. When you interact with them, they execute their commands, and evaluators determine their state post-interaction. +::: diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/WorkflowStepService.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/WorkflowStepService.md new file mode 100644 index 000000000..7070cade4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/WorkflowStepService.md @@ -0,0 +1,174 @@ +--- +sidebar_position: 9 +sidebar_label: WorkflowStep Service +--- + +# Workflow Step Service + +This service allows you to manage your workflow in smaller steps. It provides a structured way to define and navigate through different stages +or phases of a larger process or workflow. Each step can have its own configuration, layout, toolbar buttons, and other settings tailored to the specific requirements of that stage. + +## Anatomy of a Workflow Step + +The anatomy of a workflow step refers to the different components or properties that define and configure each individual step within the workflow. Each step can be customized with various settings to tailor the user interface, available tools, and behavior of the application for that specific stage of the workflow. Here are the key components that make up a workflow step: + +- `id`: A unique identifier for the step +- `name`: A human-readable name or title for the step, which can be displayed in the user interface to help users understand the current stage of the workflow. +- `hangingProtocol`: The hanging protocol configuration specifies the protocol and stage ID to be used for displaying the images. This ensures that the appropriate data viewports and presentation are used for the current workflow step. +- `layout`: The layout configuration defines the arrangement and visibility of various panels or viewports within the application's user interface for the specific step. This can include specifying which panels should be visible on the left or right side of the screen, as well as any options for panel visibility or behavior. +- `toolbarButtons`: Each step can define a set of toolbar buttons that should be available and displayed in the application's toolbar during that step. Remember the button definitions should already be registered to toolbarService beforehand, here we are just referencing the buttons id in each section. +- `info` : An optional description or additional information about the current workflow step can be provided. which +will be displayed as tooltip in the UI. + +- Step Callbacks or Commands: Some workflow steps may require specific actions or commands to be executed when the step is entered or exited. These callbacks or commands can be defined within the step configuration and can be used to update the application's state, perform data processing, or trigger other relevant actions. For instance you have access to `onEnter` hook to run a command right after the step is entered. + +For instance, a simplified example of our pre-clinical 4D workflow steps configuration might look like this: + +```js +const dynamicVolume = { + sopClassHandler: + "@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume", + leftPanel: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume", + toolBox: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox", + export: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export", +} + +const cs3d = { + segmentation: + "@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation", +} + + + +const steps = [ + { + id: "dataPreparation", + name: "Data Preparation", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + }, + }, + toolbarButtons: { + buttonSection: "primary", + buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], + }, + hangingProtocol: { + protocolId: "default4D", + stageId: "dataPreparation", + }, + info: "In the Data Preparation step...", + }, + { + id: "roiQuantification", + name: "ROI Quantification", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + right: [ + [dynamicVolume.toolBox, cs3d.segmentation, dynamicVolume.export], + ], + }, + options: { + leftPanelClosed: false, + rightPanelClosed: false, + }, + }, + toolbarButtons: [ + { + buttonSection: "primary", + buttons: [ + "MeasurementTools", + "Zoom", + "WindowLevel", + "Crosshairs", + "Pan", + ], + }, + { + buttonSection: "dynamic-toolbox", + buttons: ["BrushTools", "RectangleROIStartEndThreshold"], + }, + ], + hangingProtocol: { + protocolId: "default4D", + stageId: "roiQuantification", + }, + info: "The ROI quantification step ...", + }, + { + id: "kineticAnalysis", + name: "Kinetic Analysis", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + right: [], + }, + }, + toolbarButtons: { + buttonSection: "primary", + buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], + }, + hangingProtocol: { + protocolId: "default4D", + stageId: "kineticAnalysis", + }, + onEnter: [ + { + commandName: "updateSegmentationsChartDisplaySet", + options: { servicesManager }, + }, + ], + info: "The Kinetic Analysis step ...", + }, +] + +``` + +## Integration + +After you have defined your workflow steps, you can integrate them into your application by using the `workflowStepsService`. + +These steps should be called on `onSetupRouteComplete` in your mode factory. + + +Note: onModeEnter is too soon to call these steps as the mode is not yet fully initialized. + + +```js +onSetupRouteComplete: ({ servicesManager }) => { + workflowStepsService.addWorkflowSteps(workflowSettings.steps); + workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id); +}, +``` + +check out the `modes/preclinical-4d/src/index.tsx` for a complete example. + + +## User Interface + +We have developed a simple dropdown UI element that you can use to navigate between the different steps of your workflow. This dropdown can be added to the toolbar like below: + +```js +toolbarService.addButtons([ + { + id: 'ProgressDropdown', + uiType: 'ohif.progressDropdown', + }, +]) +toolbarService.createButtonSection('secondary', ['ProgressDropdown']); +``` + +It will appear in the `secondary` location in the toolbar. + +![alt text](../../../assets/img/progressDropdown.png) + +:::note +if you like to place the progressbar in a different location, you can use the Toolbox component +to create a button section and place the progress bar there. + +Read more in the [Toolbar module](../../extensions//modules/toolbar.md) +::: diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/services/data/_category_.json new file mode 100644 index 000000000..984ac9a42 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Data Services", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/data/index.md b/platform/docs/versioned_docs/version-3.8/platform/services/data/index.md new file mode 100644 index 000000000..d9a9c7465 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/data/index.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + +# Overview + +Data services are the first category of services which deal with handling non-ui +related state Each service have their own internal state which they handle. + +> We have replaced the _redux_ store. Instead, we have introduced various +> services and a pub/sub pattern to subscribe and run, which makes the `OHIF-v3` +> architecture nice and clean. + +We maintain the following non-ui Services: + +- [DicomMetadata Store](./../data/DicomMetadataStore.md) +- [DisplaySet Service](./../data/DisplaySetService.md) +- [Hanging Protocol Service](../data/HangingProtocolService.md) +- [Toolbar Service](./ToolbarService.md) +- [Measurement Service](../data/MeasurementService.md) +- [Customization Service](./../ui/customization-service.md) +- [State Sync Service](../data/StateSyncService.md) +- [Panel Service](../data/PanelService.md) + +## Service Architecture + +![services-data](../../../assets/img/services-data.png) + +> We have explained services and how to create a custom service in the +> [`ServicesManager`](../../managers/service.md) section of the docs + +To recap: The simplest service return a new object that has a `name` property, +and `Create` method which instantiate the service class. The "Factory Function" +that creates the service is provided with the implementation (this is slightly +different for UI Services). + +```js +// extensions/customExtension/src/services/backEndService/index.js +import backEndService from './backEndService'; + +export default function WrappedBackEndService(servicesManager) { + return { + name: 'myService', + create: ({ configuration = {} }) => { + return new backEndService(servicesManager); + }, + }; +} +``` + +A service, once created, can be registered with the `ServicesManager` to make it +accessible to extensions. Similarly, the application code can access named +services from the `ServicesManager`. + +[Read more of how to design a new custom service and register it](../../managers/service.md) diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/index.md b/platform/docs/versioned_docs/version-3.8/platform/services/index.md new file mode 100644 index 000000000..4fecc0ee4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/index.md @@ -0,0 +1,195 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Services + +## Overview + +Services are "concern-specific" code modules that can be consumed across layers. +Services provide a set of operations, often tied to some shared state, and are +made available to through out the app via the `ServicesManager`. Services are +particularly well suited to address [cross-cutting +concerns][cross-cutting-concerns]. + +Each service should be: + +- self-contained +- able to fail and/or be removed without breaking the application +- completely interchangeable with another module implementing the same interface + +> In `OHIF-v3` we have added multiple non-UI services and have introduced +> **pub/sub** pattern to reduce coupling between layers. +> +> [Read more about Pub/Sub](./pubsub.md) + +## Services + +The following services is available in the `OHIF-v3`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ServiceTypeDescription
+ + DicomMetadataStore + + Data Service + DicomMetadataStore +
+ + DisplaySetService + + Data Service + DisplaySetService +
+ + segmentationService + + Segmentation Service + segmentationService +
+ + HangingProtocolService + + Data Service + HangingProtocolService +
+ + MeasurementService (MODIFIED) + + Data Service + MeasurementService +
+ + ToolBarService + + Data Service + ToolBarService +
+ + ViewportGridService + + UI Service + ViewportGridService +
+ + Cine Service + + UI Service + cine +
+ + CustomizationService + + UI Service + customizationService +
+ + UIDialogService + + UI Service + UIDialogService +
+ + UIModalService + + UI Service + UIModalService +
+ + UINotificationService + + UI Service + UINotificationService +
+ + UIViewportDialogService + + UI Service + UIViewportDialogService +
+ + + + + +[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services +[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js +[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern + diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/pubsub.md b/platform/docs/versioned_docs/version-3.8/platform/services/pubsub.md new file mode 100644 index 000000000..036e592ef --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/pubsub.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 4 +sidebar_label: Pub Sub +--- + +# Pub sub + +## Overview + +Publish–subscribe pattern is a messaging pattern that is one of the fundamentals +patterns used in reusable software components. + +In short, services that implement this pattern, can have listeners subscribed +to their broadcasted events. After the event is fired, the corresponding +listener will execute the function that is registered. + +You can read more about this design pattern +[here](https://cloud.google.com/pubsub/docs/overview). + +## Example: Default Initialization + +In `Mode.jsx` we have a default initialization that demonstrates a series of +subscriptions to various events. + +```js +async function defaultRouteInit({ + servicesManager, + studyInstanceUIDs, + dataSource, +}) { + const { + DisplaySetService, + HangingProtocolService, + } = servicesManager.services; + + const unsubscriptions = []; + + const { + unsubscribe: instanceAddedUnsubscribe, + } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.INSTANCES_ADDED, + ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => { + const seriesMetadata = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + + DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient); + } + ); + + unsubscriptions.push(instanceAddedUnsubscribe); + + studyInstanceUIDs.forEach(StudyInstanceUID => { + dataSource.retrieve.series.metadata({ StudyInstanceUID }); + }); + + const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + ({ StudyInstanceUID }) => { + HangingProtocolService.run({studies, displaySets, activeStudy}); + } + ); + unsubscriptions.push(seriesAddedUnsubscribe); + + return unsubscriptions; +} +``` + +## Unsubscription + +You need to be careful if you are adding custom subscriptions to the app. Each +subscription will return an unsubscription function that needs to be executed on +component destruction to avoid adding multiple subscriptions to the same +observer. + +Below, we can see `simplified` `Mode.jsx` and the corresponding `useEffect` +where the unsubscription functions are executed upon destruction. + +```js title="platform/app/src/routes/Mode/Mode.jsx" +export default function ModeRoute(/**..**/) { + /**...**/ + useEffect(() => { + /**...**/ + + DisplaySetService.init(extensionManager, sopClassHandlers); + + extensionManager.onModeEnter(); + mode?.onModeEnter({ servicesManager, extensionManager }); + + const setupRouteInit = async () => { + if (route.init) { + return await route.init(/**...**/); + } + + return await defaultRouteInit(/**...**/); + }; + + let unsubscriptions; + setupRouteInit().then(unsubs => { + unsubscriptions = unsubs; + }); + + return () => { + extensionManager.onModeExit(); + mode?.onModeExit({ servicesManager, extensionManager }); + unsubscriptions.forEach(unsub => { + unsub(); + }); + }; + }); + return <> /**...**/ ; +} +``` diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/services/ui/_category_.json b/platform/docs/versioned_docs/version-3.8/platform/services/ui/_category_.json similarity index 100% rename from platform/docs/versioned_docs/version-2.0-deprecated/services/ui/_category_.json rename to platform/docs/versioned_docs/version-3.8/platform/services/ui/_category_.json diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/ui/cine-service.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/cine-service.md new file mode 100644 index 000000000..707b5d97f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/cine-service.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 7 +sidebar_label: CINE Service +--- + +# CINE Service + +TODO... diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/customization-service.md new file mode 100644 index 000000000..7fc5a60ee --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/customization-service.md @@ -0,0 +1,526 @@ +--- +sidebar_position: 7 +sidebar_label: Customization Service +--- +# Customization Service + +There are a lot of places where users may want to configure certain elements +differently between different modes or for different deployments. A mode +example might be the use of a custom overlay showing mode related DICOM header +information such as radiation dose or patient age. + +The use of this service enables these to be defined in a typed fashion by +providing an easy way to set default values for this, but to allow a +non-default value to be specified by the configuration or mode. + +This service is a UI service in that part of the registration allows for registering +UI components and types to deal with, but it does not directly provide an UI +displayable elements unless customized to do so. + +Note: Customization Service itself doesn't implement the actual customization, +but rather just provide mechanism to register reusable prototypes, to configure +those prototypes with actual configurations, and to use the configured objects +(components, data, whatever). +Actual implementation of the customization is totally up to the component that +supports customization. (for example, `CustomizableViewportOverlay` component uses +`CustomizationService` to implement viewport overlay that is easily customizable +from configuration.) + +## Registering customizable modules (or defining customization prototypes) + +Extensions and Modes can register customization templates they support. +It is done by adding `getCustomizationModule()` in the extension or mode definition. + +Below is the protocol of the `getCustomizationModule()`, if defined in Typescript. + +```typescript + getCustomizationModule() : { name: string, value: any }[] +``` + +If the name is 'default', it is the Default customization, which is loaded +automatically when the extension or mode is loaded. + +In the `value` of each customizations, you will define customization prototype(s). +These customization prototype(s) can be considered like "Prototype" in Javascript. +These can be used to extend the customization definitions from configurations. +Default customizations will be often used to define all the customization prototypes, +Default customizations will be often used to define all the customization prototypes, +as they will be loaded automatically along with the defining extension or mode. + + +For example, the `@ohif/extension-default` extension defines, + +```js + getCustomizationModule: () => [ + //... + + { + name: 'default', + value: [ + { + id: 'ohif.overlayItem', + content: function (props) { + if (this.condition && !this.condition(props)) return null; + + const { instance } = props; + const value = + instance && this.attribute + ? instance[this.attribute] + : this.contentF && typeof this.contentF === 'function' + ? this.contentF(props) + : null; + if (!value) return null; + + return ( + + {this.label && ( + {this.label} + )} + {value} + + ); + }, + }, + ], + }, + + //... + ], +``` + +And this `ohif.overlayItem` object will be used as a prototype (and template) to define items +to be displayed on `CustomizableViewportOverlay`. See how we use the `ohif.overlayItem` in +the example below. + +## Configuring customizations + +There are several ways to register customizations. The +`APP_CONFIG.customizationService` +field is used as a per-configuration entry. This object can list single +configurations by id, or it can list sets of customizations by referring to +the `customizationModule` in an extension. + +NOTE that these definitions from APP_CONFIG will be loaded by default, just like +extension/modes default customization. + +Below is the example configuration for `CustomizableViewportOverlay` component +customization, using the customization prototype `ohif.overlayItem` defined in +`ohif/extension-defaul` extension.: + +```js +window.config = { + //... + + // in the APP_CONFIG file set the top right area to show the patient name + // using PN: as a prefix when the study has a non-empty patient name. + customizationService: { + cornerstoneOverlayTopRight: { + id: 'cornerstoneOverlayTopRight', + items: [ + { + id: 'PatientNameOverlay', + // Note below that here we are using the customization prototype of + // `ohif.overlayItem` which was registered to the customization module in + // `ohif/extension-default` extension. + customizationType: 'ohif.overlayItem', + // the following props are passed to the `ohif.overlayItem` prototype + // which is used to render the overlay item based on the label, color, + // conditions, etc. + attribute: 'PatientName', + label: 'PN:', + title: 'Patient Name', + color: 'yellow', + condition: ({ instance }) => + instance && + instance.PatientName && + instance.PatientName.Alphabetic, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName.Alphabetic) + + ' ' + + (instance.PatientSex ? '(' + instance.PatientSex + ')' : ''), + }, + ], + }, + }, + + //... +} +``` + +In the customization configuration, you can use `customizationType` fields to +define the prototype that customization object should inherit from. +The `customizationType` field is simply the id of another customization object. + + +## Implementing customization using CustomizationService + +### Mode Customizations + +Mode-specific customizations are no different from the global ones, +except that the mode customizations are specific to one mode and +are not globally applied. Mode-specific customizations are also cleared +before the mode `onModeEnter` is called, and they can have new values registered in the `onModeEnter` + +Following on our example above to customize the overlay, we can now add a mode customization +with a bottom-right overlay. + +```js +// Import the type from the extension itself +import OverlayUICustomization from "@ohif/cornerstone-extension"; + +// In the mode itself, customizations can be registered: +onModeEnter: { + // Note how the object can be strongly typed + const bottomRight: OverlayUICustomization = { + id: 'cornerstoneOverlayBottomRight', + // Note the type is the previously registered ohif.cornerstoneOverlay + customizationType: 'ohif.cornerstoneOverlay', + // The cornerstoneOverlay definition requires an items list here. + items: [ + // Custom definitions for the context menu here. + ], + }; + customizationService.addModeCustomizations(bottomRight); +} +``` + +The mode customizations are retrieved via the `getModeCustomization` function, +providing an id, and optionally a default value. The retrieval will return, +in order: + +1. Global customization with the given id. +2. Mode customization with the id. +3. The default value specified. + +The return value then inherits the `customizationType` instance, so that the +value can be typed and have default values and functionality provided. The object +can then be used in a way defined by the extension provided that customization +point. + +```ts +const cornerstoneOverlay = customizationService.getModeCustomization( + "cornerstoneOverlay", + { customizationType: "ohif.cornerstoneOverlay" }, +); + +const { component: overlayComponent, props } = + customizationService.getComponent(cornerstoneOverlay); + +return ( + +); +``` + +This example shows fetching the default component to render this object. The +returned object would be a sub-type of ohif.cornerstoneOverlay if defined. This +object can be a React component or other object such as a commands list, for +example (this example comes from the context menu customizations as that one +uses commands lists): + +```ts +cornerstoneContextMenu = customizationService.get( + "cornerstoneContextMenu", + defaultMenu, +); +commandsManager.run(cornerstoneContextMenu, extraProps); +``` + +### Global Customizations + +Global customizations are retrieved in the same was as mode customizations, except +that the `getGlobalCustomization` is called instead of the mode call. + +### Types + +Some types for the customization service are provided by the `@ohif/ui` types +export. Additionally, extensions can provide a Types export with custom +typing, allowing for better typing for the extension specific capabilities. +This allows for having strong typing when declaring customizations, for example: + +```ts +import { Types } from '@ohif/ui'; + +const customContextMenu: Types.ContextMenu.Menu = + { + id: 'cornerstoneContextMenu', + customizationType: 'ohif.contextMenu', + // items will be type checked to be in accordance with UIContextMenu.items + items: [ ... ] + }, +``` + +### Inheritance + +JavaScript property inheritance can be supplied by defining customizations +with id corresponding to the customizationType value. For example: + +```js +getCustomizationModule = () => ([ + { + name: 'default', + value: [ + { + id: 'ohif.overlayItem', + content: function (props) { + return (

{this.label} {props.instance[this.attribute]}

) + }, + }, + ], + } +]) +``` + +defines an overlay item which has a React content object as the render value. +This can then be used by specifying a `customizationType` of `ohif.overlayItem`, for example: + +```js +const overlayItem: Types.UIOverlayItem = { + id: 'anOverlayItem', + customizationType: 'ohif.overlayItem', + attribute: 'PatientName', + label: 'PN:', +}; +``` + +# Customizations + +This section can be used to specify various customization capabilities. + +## Text color for StudyBrowser tabs + +This is the recommended pattern for deep customization of class attributes, +making it fine grained, and have it apply a set of attributes, mostly from +tailwind. In this case it is a double indirection, as the buttons class +uses it's own internal class names. + +* Name: 'class:StudyBrowser' +* Attributes: +** `true` for the is active true text color +** `false` for the is active false text color. +** Values are button colors, from the Button class, eg default, white, black + +## customRoutes + +* Name: `customRoutes` global +* Attributes: +** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add. +** Should any element of routes match an existing baked in element, the baked in one will be replaced. +** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes) + +### Example + +```js +{ + id: 'customRoutes', + routes: [ + { + path: '/myroute', + children: MyRouteReactFunction, + } + ], +} +``` + +There is a usage of this example commented out in config/default.js that +looks like the code below. This example is provided by the default extension, +again with commented out code. Uncomment the getCustomizationModule customRoutes +code in the default module to activate this, and then go to: `http://localhost:3000/custom` +to see the custom route. + +Note the name of this is the customization module name, which usually won't match +the id, and in fact there can be multiple customization objects defined for a single +customization module, to allow for customizing sets of related values. + +```js +customizationService: [ + // Shows a custom route -access via http://localhost:3000/custom + '@ohif/extension-default.customizationModule.helloPage', +], +``` + +## Customizable Viewport Overlay + +Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay. + +```javascript +// this is one of the configuration files in `platform/app/public/config/*.js` +window.config = { + // ... + + customizationService: { + cornerstoneOverlayTopLeft: { + id: 'cornerstoneOverlayTopLeft', + items: [ + { + id: 'WindowLevel', + customizationType: 'ohif.overlayItem.windowLevel', + }, + { + id: 'PatientName', + customizationType: 'ohif.overlayItem', + label: '', + color: 'green', + background: 'white', + condition: ({ instance }) => + instance && instance.PatientName && instance.PatientName.Alphabetic, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName.Alphabetic) + + ' ' + + (instance.PatientSex ? '(' + instance.PatientSex + ')' : ''), + }, + { + id: 'Species', + customizationType: 'ohif.overlayItem', + label: 'Species:', + condition: ({ instance }) => + instance && instance.PatientSpeciesDescription, + contentF: ({ instance }) => + instance.PatientSpeciesDescription + + '/' + + instance.PatientBreedDescription, + }, + { + id: 'PID', + customizationType: 'ohif.overlayItem', + label: 'PID:', + title: 'Patient PID', + condition: ({ instance }) => instance && instance.PatientID, + contentF: ({ instance }) => instance.PatientID, + }, + { + id: 'PatientBirthDate', + customizationType: 'ohif.overlayItem', + label: 'DOB:', + title: "Patient's Date of birth", + condition: ({ instance }) => instance && instance.PatientBirthDate, + contentF: ({ instance }) => instance.PatientBirthDate, + }, + { + id: 'OtherPid', + customizationType: 'ohif.overlayItem', + label: 'Other PID:', + title: 'Other Patient IDs', + condition: ({ instance }) => instance && instance.OtherPatientIDs, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.OtherPatientIDs), + }, + ], + }, + cornerstoneOverlayTopRight: { + id: 'cornerstoneOverlayTopRight', + + items: [ + { + id: 'InstanceNmber', + customizationType: 'ohif.overlayItem.instanceNumber', + }, + { + id: 'StudyDescription', + customizationType: 'ohif.overlayItem', + label: '', + title: ({ instance }) => + instance && + instance.StudyDescription && + `Study Description: ${instance.StudyDescription}`, + condition: ({ instance }) => instance && instance.StudyDescription, + contentF: ({ instance }) => instance.StudyDescription, + }, + { + id: 'StudyDate', + customizationType: 'ohif.overlayItem', + label: '', + title: 'Study date', + condition: ({ instance }) => instance && instance.StudyDate, + contentF: ({ instance, formatters: { formatDate } }) => + formatDate(instance.StudyDate), + }, + { + id: 'StudyTime', + customizationType: 'ohif.overlayItem', + label: '', + title: 'Study time', + condition: ({ instance }) => instance && instance.StudyTime, + contentF: ({ instance, formatters: { formatTime } }) => + formatTime(instance.StudyTime), + }, + ], + }, + cornerstoneOverlayBottomLeft: { + id: 'cornerstoneOverlayBottomLeft', + + items: [ + { + id: 'SeriesNumber', + customizationType: 'ohif.overlayItem', + label: 'Ser:', + title: 'Series Number', + condition: ({ instance }) => instance && instance.SeriesNumber, + contentF: ({ instance }) => instance.SeriesNumber, + }, + { + id: 'SliceLocation', + customizationType: 'ohif.overlayItem', + label: 'Loc:', + title: 'Slice Location', + condition: ({ instance }) => instance && instance.SliceLocation, + contentF: ({ instance, formatters: { formatNumberPrecision } }) => + formatNumberPrecision(instance.SliceLocation, 2) + ' mm', + }, + { + id: 'SliceThickness', + customizationType: 'ohif.overlayItem', + label: 'Thick:', + title: 'Slice Thickness', + condition: ({ instance }) => instance && instance.SliceThickness, + contentF: ({ instance, formatters: { formatNumberPrecision } }) => + formatNumberPrecision(instance.SliceThickness, 2) + ' mm', + }, + ], + }, + }, + + // ... +} +``` + + + +## Context Menus + +Context menus can be created by defining the menu structure and click +interaction, as defined in the `ContextMenu/types`. There are examples +below specific to the cornerstone context, because the actual click +handler and attributes used to decide when and how to display the menu +are specific to the context used for where the menu is displayed. + +## Cornerstone Context Menu + +The default cornerstone context menu can be customized by setting the +`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`. + +## Customizable Cornerstone Viewport Click Behaviour + +The behaviour on clicking on the cornerstone viewport can be customized +by setting the `cornerstoneViewportClickCommands`. This is intended to +support both the cornerstone 3D internal commands as well as things like +context menus. Currently it supports buttons 1-3, as well as modifier keys +by associating a commands list with the button to click. See `initContextMenu` +for more details. + +## Please add additional customizations above this section +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js +[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js +[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/ui/index.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/index.md new file mode 100644 index 000000000..2a0203a74 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/index.md @@ -0,0 +1,304 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + +# Overview + + + +A typical web application will have components and state for common UI like +modals, notifications, dialogs, etc. A UI service makes it possible to leverage +these components from an extension. + +We maintain the following UI Services: + +- [UI Notification Service](ui-notification-service.md) +- [UI Modal Service](ui-modal-service.md) +- [UI Dialog Service](ui-dialog-service.md) +- [UI Viewport Dialog Service](ui-viewport-dialog-service.md) +- [CINE Service](cine-service.md) +- [Viewport Grid Service](viewport-grid-service.md) + + + +![UIService](../../../assets/img/ui-services.png) + + + +## Providers for UI services + +**There are several context providers that wraps the application routes. This +makes the context values exposed in the app, and service's `setImplementation` +can get run to override the implementation of the service.** + +```js title="platform/app/src/App.jsx" +function App({ config, defaultExtensions }) { + /**...**/ + /**...**/ + return ( + /**...**/ + + + + + + + {appRoutes} + + + + + + + /**...**/ + ); +} +``` + +## Example + +For instance `UIModalService` has the following Public API: + +```js title="platform/core/src/services/UIModalService/index.js" +const publicAPI = { + name, + hide: _hide, + show: _show, + setServiceImplementation, +}; + +function setServiceImplementation({ + hide: hideImplementation, + show: showImplementation, +}) { + /** ... **/ + serviceImplementation._hide = hideImplementation; + serviceImplementation._show = showImplementation; + /** ... **/ +} + +export default { + name: 'UIModalService', + create: ({ configuration = {} }) => { + return publicAPI; + }, +}; +``` + +`UIModalService` implementation can be set (override) in its context provider. +For instance in `ModalProvider` we have: + +```js title="platform/ui/src/contextProviders/ModalProvider.jsx" +import { Modal } from '@ohif/ui'; + +const ModalContext = createContext(null); +const { Provider } = ModalContext; + +export const useModal = () => useContext(ModalContext); + +const ModalProvider = ({ children, modal: Modal, service }) => { + const DEFAULT_OPTIONS = { + content: null, + contentProps: null, + shouldCloseOnEsc: true, + isOpen: true, + closeButton: true, + title: null, + customClassName: '', + }; + + const show = useCallback(props => setOptions({ ...options, ...props }), [ + options, + ]); + + const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [ + DEFAULT_OPTIONS, + ]); + + useEffect(() => { + if (service) { + service.setServiceImplementation({ hide, show }); + } + }, [hide, service, show]); + + const { + content: ModalContent, + contentProps, + isOpen, + title, + customClassName, + shouldCloseOnEsc, + closeButton, + } = options; + + return ( + + {ModalContent && ( + + + + )} + {children} + + ); +}; + +export default ModalProvider; + +export const ModalConsumer = ModalContext.Consumer; +``` + +Therefore, anywhere in the app that we have access to react context we can use +it by calling the `useModal` from `@ohif/ui`. As a matter of fact, we are +utilizing the modal for the preference window which shows the hotkeys after +clicking on the gear button on the right side of the header. + +A `simplified` code for our worklist is: + +```js title="platform/app/src/routes/WorkList/WorkList.jsx" +import { useModal, Header } from '@ohif/ui'; + +function WorkList({ + history, + data: studies, + dataTotal: studiesTotal, + isLoadingData, + dataSource, + hotkeysManager, +}) { + const { show, hide } = useModal(); + + /** ... **/ + + const menuOptions = [ + { + title: t('Header:About'), + icon: 'info', + onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }), + }, + { + title: t('Header:Preferences'), + icon: 'settings', + onClick: () => + show({ + title: t('UserPreferencesModal:User Preferences'), + content: UserPreferences, + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions( + hotkeyDefaults + ), + hotkeyDefinitions, + onCancel: hide, + currentLanguage: currentLanguage(), + availableLanguages, + defaultLanguage, + onSubmit: state => { + i18n.changeLanguage(state.language.value); + hotkeysManager.setHotkeys(state.hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), + }, + }), + }, + ]; + /** ... **/ + return ( +
+ /** ... **/ +
+ /** ... **/ +
+ ); +} +``` + + + + + + + +## Tips & Tricks + +It's important to remember that all we're doing is making it possible to control +bits of the application's UI from an extension. Here are a few non-obvious +takeaways worth mentioning: + +- Your application code should continue to use React context + (consumers/providers) as it normally would +- You can substitute our "out of the box" UI implementations with your own +- You can create and register your own UI services +- You can choose not to register a service or provide a service implementation +- In extensions, you can provide fallback/alternative behavior if an expected + service is not registered + - No `UIModalService`? Use the `UINotificationService` to notify users. +- You can technically register a service in an extension and expose it to the + core application + +> Note: These are recommended patterns, not hard and fast rules. Following them +> will help reduce confusion and interoperability with the larger OHIF +> community, but they're not silver bullets. Please speak up, create an issue, +> if you would like to discuss new services or improvements to this pattern. diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/services/ui/ui-dialog-service.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-dialog-service.md similarity index 81% rename from platform/docs/versioned_docs/version-2.0-deprecated/services/ui/ui-dialog-service.md rename to platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-dialog-service.md index f25d69b41..152841ae6 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/services/ui/ui-dialog-service.md +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-dialog-service.md @@ -1,5 +1,6 @@ --- -sidebar_position: 2 +sidebar_position: 4 +sidebar_label: UI Dialog Service --- # UI Dialog Service @@ -13,14 +14,8 @@ streamlined focus. They can be helpful when: If you're curious about the DOs and DON'Ts of dialogs and modals, check out this article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] - -![UIDialogService](../../assets/img/dialog-example.gif) + ## Interface For a more detailed look on the options and return values each of these methods diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/services/ui/ui-modal-service.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-modal-service.md similarity index 72% rename from platform/docs/versioned_docs/version-2.0-deprecated/services/ui/ui-modal-service.md rename to platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-modal-service.md index 8c6dd05b9..e78bc9c01 100644 --- a/platform/docs/versioned_docs/version-2.0-deprecated/services/ui/ui-modal-service.md +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-modal-service.md @@ -1,5 +1,6 @@ --- sidebar_position: 3 +sidebar_label: UI Modal Service --- # UI Modal Service @@ -14,14 +15,10 @@ be centered, and not draggable. They're commonly used when: If you're curious about the DOs and DON'Ts of dialogs and modals, check out this article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] - -![UIModalService](../../assets/img/modal-example.gif) +
+ +
## Interface @@ -35,12 +32,16 @@ is expected to support, [check out it's interface in `@ohif/core`][interface] ## Implementations -| Implementation | Consumer | -| ---------------------------------- | ----------------------------- | -| [Modal Provider][modal-provider]\* | [OHIFModal][modal-consumer]\* | +| Implementation | Consumer | +| ---------------------------------- | --------- | +| [Modal Provider][modal-provider]\* | Modal.jsx | `*` - Denotes maintained by OHIF + + + + > 3rd Party implementers may be added to this table via pull requests. -![UINotificationService](../../assets/img/notification-example.gif) + +
+ +
+ + ## Interface For a more detailed look on the options and return values each of these methods diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-viewport-dialog-service.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-viewport-dialog-service.md new file mode 100644 index 000000000..b50e3e7e4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/ui-viewport-dialog-service.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 5 +sidebar_label: UI Viewport Dialog Service +--- + +# UI Viewport Dialog Service + +## Overview +This is a new UI service, that creates a modal inside the viewport. + +Dialogs have similar characteristics to that of Modals, but often with a +streamlined focus. They can be helpful when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + + +
+ +
+ +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| -------------- | ------------------------------------------------------ | +| `create()` | Creates a new Dialog that is displayed until dismissed | +| `dismiss()` | Dismisses the specified dialog | +| `dismissAll()` | Dismisses all dialogs | + +## Implementations + +| Implementation | Consumer | +| ------------------------ | -------------------------- | +| [ViewportDialogProvider] | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + + +## State + +```js +const DEFAULT_STATE = { + viewportId: null, + message: undefined, + type: 'info', // "error" | "warning" | "info" | "success" + actions: undefined, // array of { type, text, value } + onSubmit: () => { + console.log('btn value?'); + }, + onOutsideClick: () => { + console.warn('default: onOutsideClick') + }, + onDismiss: () => { + console.log('dismiss? -1'); + }, +}; +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/services/ui/viewport-grid-service.md b/platform/docs/versioned_docs/version-3.8/platform/services/ui/viewport-grid-service.md new file mode 100644 index 000000000..186afc419 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/services/ui/viewport-grid-service.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +sidebar_label: Viewport Grid Service +--- + +# Viewport Grid Service + +## Overview + +This is a new UI service, that handles the grid layout of the viewer. + +## Events + +There are seven events that get publish in `ViewportGridService `: + +| Event | Description | +| ----------------------------- | --------------------------------------------------| +| ACTIVE_VIEWPORT_ID_CHANGED | Fires the Id of the active viewport is changed | +| LAYOUT_CHANGED | Fires the layout is changed | +| GRID_STATE_CHANGED | Fires when the entire grid state is changed | +| VIEWPORTS_READY | Fires when the viewports are ready in the grid | + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| --------------------------------------------------------------------- | --------------------------------------------------- | +| `setActiveViewportId(viewportId)` | Sets the active viewport Id in the app | +| `getState()` | Gets the states of the viewport (see below) | +| `setDisplaySetsForViewport({ viewportId, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id | +| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. | +| `reset()` | Resets the default states | +| `getNumViewportPanes()` | Gets the number of visible viewport panes | +| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state| +| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid| +| `getActiveViewportOptionByKey(key)` | Gets the specified viewport option field (key) for the active viewport | + +## Implementations + +| Implementation | Consumer | +| ---------------------- | -------------------------- | +| [ViewportGridProvider] | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + +## State + +```js +const DEFAULT_STATE = { + // starting from null, hanging + // protocol will defined number of rows and cols + numRows: null, + numCols: null, + viewports: [ + /* + * { + * displaySetInstanceUID: string, + * } + */ + ], + activeViewportId: null, +}; +``` diff --git a/platform/docs/versioned_docs/version-3.8/platform/themeing.md b/platform/docs/versioned_docs/version-3.8/platform/themeing.md new file mode 100644 index 000000000..48a881c20 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/platform/themeing.md @@ -0,0 +1,166 @@ +--- +sidebar_position: 2 +sidebar_label: Theming +--- + +# Viewer: Theming + +`OHIF-v3` has introduced the +[`LayoutTemplateModule`](./extensions/modules/layout-template.md) which enables +addition of custom layouts. You can easily design your custom components inside +an extension and consume it via the layoutTemplate module you write. + +## Tailwind CSS + +[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for +creating custom user interfaces. + +Below you can see a compiled version of the tailwind configs. Each section can +be edited accordingly. For instance screen size break points, primary and +secondary colors, etc. + +```js +module.exports = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + }, + colors: { + overlay: 'rgba(0, 0, 0, 0.8)', + transparent: 'transparent', + black: '#000', + white: '#fff', + initial: 'initial', + inherit: 'inherit', + + indigo: { + dark: '#0b1a42', + }, + aqua: { + pale: '#7bb2ce', + }, + + primary: { + light: '#5acce6', + main: '#0944b3', + dark: '#090c29', + active: '#348cfd', + }, + + secondary: { + light: '#3a3f99', + main: '#2b166b', + dark: '#041c4a', + active: '#1f1f27', + }, + + common: { + bright: '#e1e1e1', + light: '#a19fad', + main: '#fff', + dark: '#726f7e', + active: '#2c3074', + }, + + customgreen: { + 100: '#05D97C', + }, + + customblue: { + 100: '#c4fdff', + 200: '#38daff', + }, + }, + }, +}; +``` + +You can also use the color variable like before. For instance: + +```js +primary: { + default: ‘var(--default-color)‘, + light: ‘#5ACCE6’, + main: ‘#0944B3’, + dark: ‘#090C29’, + active: ‘#348CFD’, +} +``` + +## White Labeling + +A white-label product is a product or service produced by one company (the +producer) that other companies (the marketers) rebrand to make it appear as if +they had made it - +[Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product) + +Current white-labeling options are limited. We expose the ability to replace the +"Logo" section of the application with a custom "Logo" component. You can do +this by adding a whiteLabeling key to your configuration file. + +```js +window.config = { + /** .. **/ + whiteLabeling: { + createLogoComponentFn: function(React) { + return React.createElement( + 'a', + { + target: '_blank', + rel: 'noopener noreferrer', + className: 'text-white underline', + href: 'http://radicalimaging.com', + }, + React.createElement('h5', {}, 'RADICAL IMAGING') + ); + }, + }, + /** .. **/ +}; +``` + +> You can simply use the stylings from tailwind CSS in the whiteLabeling + +In addition to text, you can also add your custom logo + +```js +window.config = { + /** .. **/ + whiteLabeling: { + createLogoComponentFn: function(React) { + return React.createElement( + 'a', + { + target: '_self', + rel: 'noopener noreferrer', + className: 'text-purple-600 line-through', + href: '/', + }, + React.createElement('img', { + src: './customLogo.svg', + // className: 'w-8 h-8', + }) + ); + }, + }, + /** .. **/ +}; +``` + +The output will look like + +![custom-logo](../assets/img/custom-logo.png) + + + + +[wikipedia]: https://en.wikipedia.org/wiki/White-label_product + diff --git a/platform/docs/versioned_docs/version-3.8/release-notes.md b/platform/docs/versioned_docs/version-3.8/release-notes.md new file mode 100644 index 000000000..48491be16 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/release-notes.md @@ -0,0 +1,10 @@ +--- +sidebar_position: 2 +sidebar_label: Release Notes +--- + +# Release Notes + + + +You can find the detailed release notes on the OHIF website. Please visit [https://ohif.org/release-notes](https://ohif.org/release-notes) diff --git a/platform/docs/versioned_docs/version-3.8/resources.md b/platform/docs/versioned_docs/version-3.8/resources.md new file mode 100644 index 000000000..1fad4361f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/resources.md @@ -0,0 +1,155 @@ +--- +sidebar_position: 9 +sidebar_label: Resources +--- + +# Resources + +Throughout the development of the OHIF Viewer, we have participated in various +conferences and "hackathons". In this page, we will provide the presentations +and other resources that we have provided to the community in the past: + +## 2023 + +### IMNO 2024 - March 19-20, 2024 + +We participated in the Imaging Network Ontario (ImNO) 2024 symposium, presenting three posters. One of our presentations received the best talk award during the session. + + +- Advancing Medical Imaging on the Web: Implementation of Hanging Protocols for Automated Image Display Configuration in OHIF V3 [Poster](https://www.dropbox.com/scl/fi/z4h86bmsxi0c62e1n6h9l/P7-9-Alireza-Sedghi-Final.pdf?rlkey=v5pm0p5ygkbq41x9bz3hr5yi8&dl=0) +- Advancing Medical Imaging on the Web: Optimizing the Dicomweb Server Architecture with Static Dicomweb [Poster](https://www.dropbox.com/scl/fi/ep0lxjp90kbxhjoffe4kh/P7-10-Bill-Wallace-Final.pdf?rlkey=xl2u6tdnh9j9hgvkajxv3b02o&dl=0) +- (**🏆🏆 BEST PRESENTATION AWARD in the Session 7 Pitches: Devices, HW, SW Development 🏆🏆**) Advancing Medical Imaging on the Web: Integrating High Throughput JPEG 2000 (HTJ2K) in Cornerstone3D for Streamlined Progressive Loading and Visualization [Poster](https://www.dropbox.com/scl/fi/srs2rxgtv2r69ver9ub1j/P7-8-Bill-Wallace-Final.pdf?rlkey=k9mmraw76r9q2s3b9w9s0793w&dl=0) + + +### ITCR 2023 Conference | September 11-13, 2023 + +Dr. Gordon Harris presented an update on OHIF in [NCI Informatics Technology for Cancer Research Annual Meeting](https://www.itcr2023.org/). You can find the slides and poster here: +[[Slides]](https://docs.google.com/presentation/d/1R38s95db_yZj0WoYdlUbaWGZsWVb3H-3u_hXBZXiTaE/edit?usp=sharing)[[Poster]](https://ohif-assets.s3.us-east-2.amazonaws.com/presentations/OHIF-ITCR-2023-FINAL-PRINT.pdf) + + + + +### SIIM 2023 Tech Tools Webinar | April 12th, 2023 + +Free, Open Source Tools for Research: MONAI and OHIF Viewer +[[Slides](https://docs.google.com/presentation/d/1afJ5Y9Tzukgn7eAbaO1oiCtN7XvIimFdmZP-HcOUofA/edit?usp=sharing)][[Video](https://www.youtube.com/watch?v=lo8J5w5jUJI)] + + +### NA-MIC Project Week 38th 2023 - Remote + +We participated in the 38th Project Week with three projects around OHIF. [[Website](https://projectweek.na-mic.org/PW38_2023_GranCanaria/)] + +- PolySeg representations for OHIF Viewer ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_PolySeg/)) +- Cross study synchronizer for OHIF Crosshair ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_SyncCrosshair/)) +- DATSCAN Viewer implementation in OHIF ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_DATSCAN/)) + + + +## 2022 + +### OHIF Demo to Interns +[[Slides]](https://docs.google.com/presentation/d/1a2PkUnqkVMaXaBsuFn7-PPlBJULU3dBwzI_44gKFeYI/edit?usp=sharing) + +### SIIM 2022 - Updates from the Imaging Informatics Community +We participated in the SIIM 2022 conference to give update for the imaging +informatics community. +[[Slides]](https://docs.google.com/presentation/d/1EUGaUzQtGhZbZWpGLe6ONqChpVMw9Qr9l3KHODevMow/edit?usp=sharing) +[[Video]](https://vimeo.com/734463662/dbd5a88371) + +### The Imaging Network Ontario - Remote + +The Imaging Network Ontario (ImNO) is an annual symposium that brings together +medical imaging researchers and scientists from across Canada to share +knowledge, ideas, and experiences. +[[Slides]](https://docs.google.com/presentation/d/18XZDon4-Sitc2a70V5sFyhyUVZI_mIgfXHGtdxhZMjE/edit?usp=sharing) +[[Video]](https://vimeo.com/843234581/ad7d308a44) + + +### [NA-MIC Project Week 36th 2022 - Remote](https://github.com/NA-MIC/ProjectWeek/blob/master/PW36_2022_Virtual/README.md) + +The Project Week is a week-long hackathon of hands-on activity in which medical +image computing researchers. OHIF team participated and gave a talk on OHIF and +Cornerstone in the 36th Project Week: +[[Slides]](https://docs.google.com/presentation/d/1-GtOKmr2cQi-r3OFyseSmgLeurtB3KXUkGMx2pVLh1I/edit?usp=sharing) +[[Video]](https://vimeo.com/668339696/63a2c48de8) + +## 2021 + +### [NA-MIC Project Week 35th 2021 - Remote](https://github.com/NA-MIC/ProjectWeek/tree/master/PW35_2021_Virtual) + +The Project Week is a week-long hackathon of hands-on activity in which medical +image computing researchers. OHIF team participated in the 35th Project Week +in 2021. +[[Slides]](https://docs.google.com/presentation/d/1KYNjuiI8lT1foQ4P9TGNV0lBhM6H-5KBs0wkYj4JJbk/edit?usp=sharing) + +### Chan Zuckerberg Initiative (CZI) + +Project presentations and demonstrations of Essential Open Source Software for +Science (EOSS) grantees +[[Slides]](https://docs.google.com/presentation/d/1_CLtG2hsL3ZxOtV2olVnzBOzq-TMLrHLomOy3FiU4NE/edit?usp=sharing) +[[Video]](https://youtu.be/0FjKkTJO0Rc?t=3737) + +### Google Cloud Tech + +Healthcare Imaging with Cloud Healthcare API +[[Video]](https://www.youtube.com/watch?v=2MiX9ScHFhY) + +## 2020 + +### OHIF ITCR Pitch + +OHIF pitch for Informatics Technology for Cancer Research (ITCR) +[[Slides]](https://docs.google.com/presentation/d/1MZXnZrVAnjmhVIWqC-aRSvJOoMMRLhLddACdCa1TybM/edit?usp=sharing) +[[Video]](https://vimeo.com/843234613/625bdb8793) + +## 2019 + +### OHIF and VTK.js Training Course + +OHIF and Kitware collaboration to create a training course for OHIF and VTK.js +developers. Funding for this work was provided by Kitware (NIH NINDS +R44NS081792, NIH NINDS R42NS086295, NIH NIBIB and NIGMS R01EB021396, NIH NIBIB +R01EB014955), Isomics (NIH P41 EB015902), and Massachusetts General Hospital +(NIH U24 CA199460). + +1. Introduction to VTK.js and OHIF + [[Slides]](https://docs.google.com/presentation/d/1NCJxpfx_qUGJI_2DhbECzaOg0k-Z6b65QlUptCofN-A/edit#slide=id.p) + [[Video]](https://vimeo.com/375520781) +2. Developing with VTK.js + [[Slides]](https://docs.google.com/presentation/d/17TCS6EhFi6SWFIrcAJ-DFdFzFFL-WD9BBTv-owmMdDU/edit#slide=id.p) + [[Video]](https://vimeo.com/375521036) +3. VTK.js Architecture and Tooling + [[Slides]](https://docs.google.com/presentation/d/1Sr1OGxMSw0oCt46koKQbmwSIE11Kqq8MGtyW3W0ASpk/edit?usp=gmail_thread) + [[Video]](https://vimeo.com/375521810) +4. OHIF + VTK.js Integration + [[Slides]](https://docs.google.com/presentation/d/1Iwg-u01HGVf1CgC6NbcBD3gm3uHN9WhjU59FSz55TN8/edit?ts=5d9c9ce4#slide=id.g59aa99cda4_0_131) + [[Video]](https://vimeo.com/375521206) + +## 2017 + +### Lesion Tracker + +LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer +Imaging Research and Clinical Trials. This project was supported in part by +grant U24 CA199460 from the National Cancer Institute (NCI) Informatics +Technology for Cancer Research (ITCR) Program. +[[Video]](https://www.youtube.com/watch?v=gUIPtoSBL-Q) + +### OHIF Community Meeting - June + +[[Slides]](https://docs.google.com/presentation/d/1K9Y6eP5DYTXoDlfwCZE6GkCUp83AK4_40YQS0dlzVBo/edit?usp=sharing) + +## 2016 + +### Imaging Community Call + +Open Source Oncology Web Viewer; Presentation by Gordon J. Harris +[[Slides]](https://www.slideshare.net/imgcommcall/lesiontracker) + +### OHIF Community Meeting - June + +[[Slides]](https://docs.google.com/presentation/d/1Ai25mBG0ZWUPhaadp3VnbCVmkYs9K51sQ8osMixrvJ0/edit?usp=sharing) + +### OHIF Community Meeting - September + +[[Slides]](https://docs.google.com/presentation/d/1iYZoU7v7KHSLHiKwH1_9_wweAkG7RGnyxrWeeHva4zQ/edit?usp=sharing) diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/_category_.json b/platform/docs/versioned_docs/version-3.8/user-guide/_category_.json new file mode 100644 index 000000000..68ea78e78 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "User Guide", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/index.md b/platform/docs/versioned_docs/version-3.8/user-guide/index.md new file mode 100644 index 000000000..52f44bad8 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/index.md @@ -0,0 +1,83 @@ +--- +sidebar_position: 2 +sidebar_label: Study List +--- + +# Study List + +## Overview + +The first page you will see when the viewer is loaded is the `Study List`. In +this page you can explore all the studies that are stored on the configured +server for the `OHIF Viewer`. + +![user-study-list](../assets/img/user-study-list.png) + +## Sorting + +When the Study List is opened, the application queries the PACS for 101 studies +by default. If there are greater than 100 studies returned, the default sort for +the study list is dictated by the image archive that hosts these studies for the +viewer and study list sorting will be disabled. If there are less than or equal +to 100 studies returned, they will be sorted by study date (most recent to +oldest) and study list sorting will be enabled. Whenever a query returns greater +than 100 studies, use filters to narrow results below 100 studies to enable +Study List sorting. + +## Filters + +There are certain filters that can be used to limit the study list to the +desired criteria. + +- Patient Name: Searches between patients names +- MRN: Searches between patients Medical Record Number +- Study Date: Filters the date of the acquisition +- Description: Searches between study descriptions +- Modality: Filters the modalities +- Accession: Searches between patients accession number + +An example of using study list filter is shown below: + +![user-study-filter](../assets/img/user-study-filter.png) + +Below the study list are pagination options for 25, 50, or 100 studies per page. + +![user-study-next](../assets/img/user-study-next.png) + +## Study Summary + +Click on a study to expand the study summary panel. + +![user-study-summary](../assets/img/user-study-summary.png) + +A summary of series available in the study is shown, which contains the series +description, series number, modality of the series, instances in the series, and +buttons to launch viewer modes to display the study. + +## Study Specific Modes + +All available modes are seen in the study expanded view. Modes can be enabled or +disabled for a study based on the modalities contained within the study. + +In the screenshot below, there are two modes shown for the selected study + +- Basic Viewer: Default mode that enables rendering and measurement tracking + +- PET/CT Fusion: Mode for visualizing the PET CT study in a 3x3 format. + +Based on the mode configurations (e.g., available modalities), PET/CT mode is +disabled for studies that do not contain PET AND CT images. + + + +![user-studyist-modespecific](../assets/img/user-studyist-modespecific.png) + +The previous screenshot shows a study containing PET and CT images and both +Basic Viewer and PET/CT Mode are available. + +## View Study + +The `Basic Viewer` mode is available for all studies by default. Click on the +mode button to launch the viewer. + +![user-open-viewer](../assets/img/user-open-viewer.png) diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/Language.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/Language.md new file mode 100644 index 000000000..de064876c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/Language.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 8 +--- + +# Language + +OHIF supports internationalization capabilities and setting the general language +of the Viewer. + +It should be noted that we don't have complete translations for all the components +and all the languages; however, you can easily add the key value translation pairs +following developer guides. + +Summary of language changing usage can be seen below: + + + +## Overview Video + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/_category_.json b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/_category_.json new file mode 100644 index 000000000..417861dba --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Basic Viewer", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/hotkeys.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/hotkeys.md new file mode 100644 index 000000000..5d8aac1fb --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/hotkeys.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 7 +--- + +# Hotkeys + +To open the hotkey assignment panel, you can click on the Preferences gear on the +top right side of the viewer. + + +Below, you can see the default hotkeys key bindings: + +![user-hotkeys-default](../../assets/img/user-hotkeys-default.png) + +Hotkeys can be assigned to custom bindings that persist for the duration of the browser session. diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/index.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/index.md new file mode 100644 index 000000000..6918ce3b1 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/index.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + + +# Overview +When you open a mode, viewport, toolbar and panels of the mode get shown. +It is important to note that each mode has a different UI, which serves its purpose. +Here we explain various components of `Basic Viewer` mode which includes measurement +tracking functionalities. + +Basic viewer mode (longitudinal): + +![user-viewer](../../assets/img/user-viewer.png) + +Let's break different aspects of the viewer to the main components: + +- Left Panel (study panel): displays series thumbnails with series details +- Viewport: renders the image and displays annotations +- Right Panel (measurements): displays annotations details +- Toolbar: displays tools and logo + +![user-viewer-components](../../assets/img/overview.png) + + + + +Now, we explain each component and its sub-elements in detail. diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/measurement-panel.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/measurement-panel.md new file mode 100644 index 000000000..b4a728d1c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/measurement-panel.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 3 +--- + +# Measurement Panel + +## Introduction +In `Basic Viewer` mode, the right panel is the `Measurement Panel`. The Measurement Panel can be expanded or hidden by clicking on the arrow to the left of `Measurements`. + +Select a measurement tool and mark an image to initiate measurement tracking. A pop-up will ask if you want to track measurements for the series on which the annotation was drawn. + +![user-measurement-panel-modal](../../assets/img/measurement-panel-prompt.png) + + + + + +If you select `Yes`, the series becomes a `tracked series`, and the current drawn measurement and next measurements are shown on the measurement panel on the right. + +![user-measurement-panel-tracked](../../assets/img/measurement-panel-tracked.png) + +If you select `No`, the measurement becomes temporary. The next annotation made will repeat the measurement tracking prompt. + +If you select `No, do not ask again`, all annotations made on the study will be temporary. + +![measurement-temporary](../../assets/img/measurement-temporary.png) + + +## Labeling Measurements +You can edit the measurement name by hovering over the measurement and selecting the edit icon. You can also label or relabel a measurement by right-clicking on it in the viewport. + +![user-measurement-edit](../../assets/img/measurement-panel-1.png) + + + +## Deleting a Measurement +A measurement can be deleting by dragging it outside the image in the viewport or by right-clicking on the measurement in the viewport and selecting 'Delete'. + + +## Jumping to a Measurement +Measurement navigation inside the top viewport can be used to move to previous and next measurement. + + +![measurements-prevNext](../../assets/img/measurements-prevNext.png) + +If a series containing a measurement is currently being displayed in a viewport, you can jump to display the measurement in the viewport by clicking on it in the Measurement Panel. + +## Export Measurements + +You can export the measurements by clicking on the `Export`. A CSV file will get downloaded to your local computer containing the drawn measurements. + + +![user-measurement-export](../../assets/img/user-measurement-export.png) + + +If you have set up your DICOM server to be able to store instances from the viewer, then you are able to create a report by clicking on the `Create Report`. +This will create a DICOM Structured Report (SR) from the measurements and push it +to the server. + +For instance, running the Viewer on a local DCM4CHEE: + + + +
+ +
+ +## Overview Video +An overview of measurement drawing and exporting can be seen below: + + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/measurement-tracking.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/measurement-tracking.md new file mode 100644 index 000000000..528ec78d6 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/measurement-tracking.md @@ -0,0 +1,124 @@ +--- +sidebar_position: 4 +--- + +# Measurement Tracking + +## Introduction +OHIF-V3's `Basic Viewer` implements a `Measurement Tracking` workflow. Measurement +tracking allows you to: + +- Draw annotations and have them shown in the measurement panel +- Create a report from the tracked measurement and export them as DICOM SR +- Use already exported DICOM SR to re-hydrate the measurements in the viewer + + +## Status Icon +Each viewport has a left icon indicating whether the series within the viewport +contains: + +- tracked measurement OR +- untracked measurement OR +- Structured Report OR +- Locked (uneditable) Structured Report + +In the following, we will discuss each category. + +### Tracked vs Untracked Measurements + +`OHIF-v3` implements a workflow for measurement tracking that can be seen below. + +![user-measurement-panel-modal](../../assets/img/tracking-workflow1.png) + +In summary, when you create an annotation, a prompt will be shown whether to start tracking or not. If you start the tracking, the annotation style will change to a solid line, and annotation details get displayed on the measurement panel. +On the other hand, if you decline the tracking prompt, the measurement will be considered "temporary," and annotation style remains as a dashed line and not shown on the right panel, and cannot be exported. + + +Below, you can see different icons that appear for a tracked vs. untracked series in +`OHIF-v3`. + +![tracked-not-tracked](../../assets/img/tracked-not-tracked.png) + + + +#### Overview video for starting the tracking for measurements: + + +
+ +
+ + +

+ +#### Overview video for not starting tracking for measurements: + + +
+ +
+ + +### Reading and Writing DICOM SR + +`OHIF-v3` provides full support for reading, writing and mapping the DICOM Structured +Report (SR) to interactable `Cornerstone Tools`. When you load an already exported +DICOM SR into the viewer, you will be prompted whether to track the measurements +for the series or not. + +![SR-exported](../../assets/img/SR-exported.png) + +If you click Yes, DICOM SR measurements gets re-hydrated into the viewer and +the series become a tracked series. However, If you say no and later decide to say track the measurements, you can always click on the SR button that will prompt you +with the same message again. + +![restore-exported-sr](../../assets/img/restore-exported-sr.png) + +The full workflow for saving measurements to SR and loading SR into the viewer is shown below. + +![user-measurement-panel-modal](../../assets/img/tracking-workflow2.png) +![user-measurement-panel-modal](../../assets/img/tracking-workflow3.png) + + +#### Overview video for loading DICOM SR and making a tracked series: + + +
+ +
+ +

+ +#### Overview video for loading DICOM SR and not making a tracked series: + + +
+ +
+ +

+ +
+ +
+ +### Loading DICOM SR into an Already Tracked Series + +If you have an already tracked series and try to load a DICOM SR measurements, +you will be shown the following lock icon. This means that, you can review the +DICOM SR measurement, manipulate image and draw "temporary" measurements; however, +you cannot edit the DICOM SR measurement. + + +![locked-sr](../../assets/img/locked-sr.png) + +

+ + +#### Overview video for loading DICOM SR inside an already tracked series: + + + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/study-panel.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/study-panel.md new file mode 100644 index 000000000..1b5190ffa --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/study-panel.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 2 +--- + +# Study Panel + +In `Basic Viewer` mode, the left panel includes Studies related to the current +patient. You can see three main type of studies below + +- Primary: The opened study from the study list. This study is always expanded + by default. +- Recent: All studies for the patient that contain study dates within 1 year of + the primary study +- All: All studies available for the patient contained within the source + repository + +The `Study Panel` displays the measurement tracking status of each series within +a study. As you can see in the first picture, the dashed circle on the left side +of each series demonstrates whether the series is being tracked for measurement +or not. + + + +![user-study-panel](../../assets/img/user-study-panel.png) + +Studies can be expanded or collapsed by clicking on the study information in the +Study Panel. If a series is being tracking within a study, the Measurement Panel +will display this information while the study is collapsed. + + + + diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/toolbar.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/toolbar.md new file mode 100644 index 000000000..1fb5527a0 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/toolbar.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 6 +--- + + +# Toolbar + +The four main components of the toolbar are: + +- Navigation back to the [Study List](../index.md) +- Logo and white labelling +- [Tools](#tools) +- [Preferences](#preferences) + +![user-viewer-toolbar](../../assets/img/user-viewer-toolbar.png) + + +## Tools +This section displays all the available tools inside the mode. +## Measurement tools +The basic viewer comes with the following default measurement tools: + +- Length Tool: Calculates the linear distance between two points in *mm* +- Bidirectional Tool: Creates a measurement of the longest diameter (LD) and longest perpendicular diameter (LPD) in *mm* +- Annotation: Used to create a qualitative marker with a freetext label +- Ellipse: Measures an elliptical area in *mm2* and Hounsfield Units (HU) +- Calibration Tool: Calibrate (or override) the Pixel Spacing Attribute (Physical distance in the patient between the center of each pixel, specified by a numeric pair - adjacent row spacing (delimiter) adjacent column spacing in mm) + +When a measurement tool is selected from the toolbar, it becomes the `active` tool. Use the caret to expand the measurement tools and select another tool. + + +![user-viewer-toolbar-measurements](../../assets/img/user-viewer-toolbar-measurements.png) + + +## Window/Level +The `Window/Level` tool enables manipulating the window level and window width of the rendered image. Click on the tool to enable freeform adjustment, then click and drag on the viewport to freely adjust the window/level. + +Click on the caret to expand the tool and choose from predefined W/L settings for common imaging scenarios. + + +![user-toolbar-preset](../../assets/img/user-toolbar-preset.png) + + +## Pan and Zoom +With the Zoom tool selected, click and drag the cursor on an image to adjust the zoom. The magnification level is displayed in the viewport. + +With the Pan tool selected, click and drag the cursor on an image to adjust the image position. + +## Image Capture +Click on the Camera icon to download a high quality image capture using common image formats (png, jpg) + +![user-toolbar-download-icon](../../assets/img/user-toolbar-download-icon.png) + +In the opened modal, the filename, image's width and height, and filetype and can be configured before downloading the image to your local computer. + +![user-toolbarDownload](../../assets/img/user-toolbarDownload.png) + + + +## Layout Selector +Please see the `Viewport` section for details. + + +## More Tools Menu +- Reset View: Resets all image manipulation such as position, zoom, and W/L +- Rotate Right: Flips the image 90 degrees clockwise +- Flip Horizontally: Flips the image 180 degrees horizontally +- Stack Scroll: Links all viewports containing images to scroll together +- Magnify: Click on an image to magnify a particular area of interest +- Invert: Inverts the color scale +- Cine: Toggles the Cine player control in the currently selected viewport. Click the `x` on the Cine player or click the tool again to toggle off. +- Angle: Measures an adjustable angle on an image +- Probe: Drag the probe to see pixel values +- Rectangle: Measures a rectangular area in mm^2 and HU + +When a tool is selected from the `More Tools` menu, it becomes the active tool until it is replaced by clicking on a different tool in the More Tools menu or main toolbar. + + +## Overview Video +An overview of tool usage can been seen below: + + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.8/user-guide/viewer/viewport.md b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/viewport.md new file mode 100644 index 000000000..1bfc693ca --- /dev/null +++ b/platform/docs/versioned_docs/version-3.8/user-guide/viewer/viewport.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 5 +--- + +# Viewport + +Image visualization happens at the viewport which contains canvas or canvases that +renders series. + +![user-viewer-main](../../assets/img/user-viewer-main.png) + + +By default, you can modify: + +- Zoom: right click dragging up or down +- Contrast/brightness: left click dragging up/down to change contrast, and left/right for changing brightness +- Pan: middle click dragging + + +## Changing Series for display +To change the displayed series, you can drag and drop the desired series from the left panel. Start, by dragging the thumbnail of the series, and drop it on the viewport. + +## Changing Layout +If you click on the layout icon on the toolbar, you can use the layout selector UI. After changing the layout, you can select studies for each new viewport by dragging and dropping in to the viewport. + +After changing the layout from 1x1, you will see each viewport gets tagged by a letter, +which matches its series section in the study list. + + +![user-viewer-layout](../../assets/img/user-viewer-layout.png) + + +## Overview Video +An overview of viewport layout change, and manipulation can be seen below: + + +
+ +
diff --git a/platform/docs/versioned_sidebars/version-2.0-deprecated-sidebars.json b/platform/docs/versioned_sidebars/version-2.0-deprecated-sidebars.json deleted file mode 100644 index 5dcd2439d..000000000 --- a/platform/docs/versioned_sidebars/version-2.0-deprecated-sidebars.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version-2.0/tutorialSidebar": [ - { - "type": "autogenerated", - "dirName": "." - } - ] -} diff --git a/platform/docs/versioned_sidebars/version-1.0-deprecated-sidebars.json b/platform/docs/versioned_sidebars/version-3.8-sidebars.json similarity index 67% rename from platform/docs/versioned_sidebars/version-1.0-deprecated-sidebars.json rename to platform/docs/versioned_sidebars/version-3.8-sidebars.json index 33ad457af..caea0c03b 100644 --- a/platform/docs/versioned_sidebars/version-1.0-deprecated-sidebars.json +++ b/platform/docs/versioned_sidebars/version-3.8-sidebars.json @@ -1,5 +1,5 @@ { - "version-1.0/tutorialSidebar": [ + "tutorialSidebar": [ { "type": "autogenerated", "dirName": "." diff --git a/platform/docs/versions.json b/platform/docs/versions.json index fe701e384..1becfc009 100644 --- a/platform/docs/versions.json +++ b/platform/docs/versions.json @@ -1 +1 @@ -["2.0-deprecated", "1.0-deprecated"] +["3.8"] diff --git a/platform/ui-next/.webpack/webpack.playground.js b/platform/ui-next/.webpack/webpack.playground.js deleted file mode 100644 index 7ce715d7b..000000000 --- a/platform/ui-next/.webpack/webpack.playground.js +++ /dev/null @@ -1,122 +0,0 @@ -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -// const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); - -const isDevelopment = process.env.NODE_ENV !== 'production'; -const isProdBuild = process.env.NODE_ENV === 'production'; - -const autoprefixer = require('autoprefixer'); -const tailwindcss = require('tailwindcss'); -const tailwindConfigPath = path.resolve('./tailwind.config.js'); - -const cssToJavaScript = { - test: /\.css$/, - use: [ - //'style-loader', - isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader, - { loader: 'css-loader', options: { importLoaders: 1 } }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - verbose: true, - plugins: [ - [tailwindcss(tailwindConfigPath)], - [autoprefixer('last 2 version', 'ie >= 11')], - ], - }, - }, - }, - ], -}; - -module.exports = { - mode: 'development', - entry: { - home: './src/_pages/index.tsx', - playground: './src/_pages/playground.tsx', - patterns: './src/_pages/patterns.tsx', - viewer: './src/_pages/viewer.tsx', - colors: './src/_pages/colors.tsx', - // add other pages here - }, - output: { - filename: '[name].bundle.js', - path: path.resolve(__dirname, 'dist'), - clean: true, - }, - resolve: { - extensions: ['.ts', '.tsx', '.js', '.jsx'], - // plugins: [new TsconfigPathsPlugin()], - }, - - module: { - rules: [ - ...(isProdBuild - ? [] - : [ - { - test: /\.[jt]sx?$/, - exclude: /node_modules/, - loader: 'babel-loader', - options: { - plugins: ['react-refresh/babel'], - }, - }, - ]), - cssToJavaScript, - ], - }, - plugins: [ - new CleanWebpackPlugin(), - - new HtmlWebpackPlugin({ - template: './.webpack/template.html', - chunks: ['home'], - filename: 'index.html', - }), - new HtmlWebpackPlugin({ - template: './.webpack/template.html', - chunks: ['playground'], - filename: 'playground.html', - }), - new HtmlWebpackPlugin({ - template: './.webpack/template.html', - chunks: ['patterns'], - filename: 'patterns.html', - }), - new HtmlWebpackPlugin({ - template: './.webpack/template.html', - chunks: ['viewer'], - filename: 'viewer.html', - }), - new HtmlWebpackPlugin({ - template: './.webpack/template.html', - chunks: ['colors'], - filename: 'colors.html', - }), - new MiniCssExtractPlugin({ - filename: '[name].css', - }), - isDevelopment && new ReactRefreshWebpackPlugin(), - !isDevelopment && - new MiniCssExtractPlugin({ - filename: '[name].css', - }), - ], - devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, - hot: true, - open: true, - port: 8002, - historyApiFallback: true, - devMiddleware: { - writeToDisk: true, - }, - }, -}; diff --git a/platform/ui-next/assets/data/index.ts b/platform/ui-next/assets/data/index.ts index 9698e39b3..8d63a4e7e 100644 --- a/platform/ui-next/assets/data/index.ts +++ b/platform/ui-next/assets/data/index.ts @@ -15,13 +15,13 @@ const dataList = [ title: 'Measurement Label', description: 'Description for Measurement One.', optionalField: 'Optional Info 1', - details: 'Data', + details: { primary: ['Data'], secondary: [] }, }, { id: 2, title: 'Measurement Label', description: 'Description for Measurement Two.', - details: 'Data', + details: { primary: ['Data'], secondary: [] }, }, ], }, @@ -55,64 +55,58 @@ const dataList = [ id: 6, title: 'Linear', description: 'Description for Linear.', - details: '49.2 mm', - series: 'S:2 I:1', + details: { primary: ['49.2 mm'], secondary: ['S2 I:1'] }, }, { id: 7, title: 'Bidirectional', description: 'Description for Bidirectional.', - details: 'L: 34.5 mm\nW: 23.0 mm', - series: 'S:2 I:2', + details: { primary: ['L: 34.5 mm', 'W: 23.0 mm'], secondary: ['S:2 I:2'] }, }, { id: 8, title: 'Ellipse', description: 'Description for Ellipse.', - details: '2641 mm²\nMax: 1087 HU', - series: 'S:2 I:4', + details: { primary: ['2641 mm²', 'Max: 1087 HU'], secondary: ['S:2 I:4'] }, }, { id: 9, title: 'Rectangle', description: 'Description for Rectangle.', - details: '1426 mm²\nMax: 718 HU', - series: 'S:2 I:5', + details: { primary: ['1426 mm²', 'Max: 718 HU'], secondary: ['S:2 I:5'] }, }, { id: 10, title: 'Circle', description: 'Description for Circle.', - details: '7339 mm²\nMax: 871 HU', - series: 'S:2 I:6', + details: { primary: ['7339 mm²', 'Max: 871 HU'], secondary: ['S:2 I:6'] }, }, { id: 11, title: 'Freehand ROI', description: 'Description for Freehand ROI.', - details: 'Mean: 215 HU\nMax: 947 HU\nArea: 839 mm²', - series: 'S:2 I:7', + details: { + primary: ['Mean: 215 HU', 'Max: 947 HU', 'Area: 839 mm²'], + secondary: ['S:2 I:7', 'S:3 I:7'], + }, }, { id: 12, title: 'Spline Tool', description: 'Description for Spline Tool.', - details: 'Area: 203 mm²', - series: 'S:2 I:8', + details: { primary: ['Area: 203 mm²'], secondary: ['S:2 I:8'] }, }, { id: 13, title: 'Livewire Tool', description: 'Description for Livewire Tool.', - details: '', - series: 'S:2 I:3', + details: { primary: ['Area: 203 mm²'], secondary: ['S:2 I:3'] }, }, { id: 14, title: 'Annotation Lorem ipsum dolor sit amet long measurement name continues here', description: 'Description for Annotation.', - details: '', - series: 'S:2 I:3', + details: { primary: ['Area: 203 mm²'], secondary: ['S:2 I:3'] }, }, ], }, @@ -213,21 +207,21 @@ const dataList = [ title: 'Segment 1', colorHex: '#FF6F61', description: 'Description for Segmentation One.', - details: 'SUV Peak: NaN\nVolume: 21.56mm³', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] }, }, { id: 30, title: 'Segment 2', colorHex: '#00CED1', description: 'Description for Segmentation Two.', - details: 'SUV Peak: NaN\nVolume: 21.56mm³', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] }, }, { id: 31, title: 'Segment 3', colorHex: '#88B04B', description: 'Description for Segmentation One.', - details: 'SUV Peak: NaN\nVolume: 21.56mm³', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] }, }, ], }, @@ -239,21 +233,21 @@ const dataList = [ title: 'Segment A', colorHex: '#FF6F61', description: 'Description for Segmentation One.', - details: 'SUV Peak: NaN\nVolume: 21.56mm³', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] }, }, { id: 33, title: 'Segment B', colorHex: '#00CED1', description: 'Description for Segmentation Two.', - details: 'SUV Peak: NaN\nVolume: 21.56mm³', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] }, }, { id: 34, title: 'Segment C', colorHex: '#88B04B', description: 'Description for Segmentation One.', - details: 'SUV Peak: NaN\nVolume: 21.56mm³', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] }, }, ], }, diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json index cd7341d10..2e63e746d 100644 --- a/platform/ui-next/package.json +++ b/platform/ui-next/package.json @@ -57,7 +57,8 @@ "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss": "3.2.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "sanitize-html": "^2.13.1" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.16.7" diff --git a/platform/ui-next/src/components/Command/index.ts b/platform/ui-next/src/components/Command/index.ts index 6ec0124d6..dabb8c4f1 100644 --- a/platform/ui-next/src/components/Command/index.ts +++ b/platform/ui-next/src/components/Command/index.ts @@ -1,4 +1,14 @@ -import { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } from './Command'; +import { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} from './Command'; export { Command, diff --git a/platform/ui-next/src/components/DataRow/DataRow.tsx b/platform/ui-next/src/components/DataRow/DataRow.tsx index 3e46d6d6e..22ddb98b0 100644 --- a/platform/ui-next/src/components/DataRow/DataRow.tsx +++ b/platform/ui-next/src/components/DataRow/DataRow.tsx @@ -7,193 +7,321 @@ import { DropdownMenuItem, } from '../../components/DropdownMenu'; import { Icons } from '../../components/Icons/Icons'; -import { - Tooltip, - TooltipTrigger, - TooltipContent, - TooltipProvider, -} from '../../components/Tooltip/Tooltip'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip'; +import sanitizeHtml from 'sanitize-html'; +/** + * DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data. + * It's designed to show a numbered item with a title, optional color indicator, and expandable details. + * The row supports various interactive features like visibility toggling, locking, and contextual actions. + * + * @component + * @example + * ```tsx + * {}} + * onToggleLocked={() => {}} + * onRename={() => {}} + * onDelete={() => {}} + * onColor={() => {}} + * /> + * ``` + */ + +/** + * Props for the DataRow component + * @interface DataRowProps + * @property {number} number - The display number/index of the row + * @property {string} title - The main text label for the row + * @property {boolean} disableEditing - When true, prevents rename and delete operations + * @property {string} [colorHex] - Optional hex color code to display a color indicator + * @property {Object} [details] - Optional hierarchical details to display below the row + * @property {string[]} details.primary - Primary details shown immediately below the row + * @property {string[]} details.secondary - Secondary details (currently unused) + * @property {boolean} [isSelected] - Whether the row is currently selected + * @property {() => void} [onSelect] - Callback when the row is clicked/selected + * @property {boolean} isVisible - Controls the row's visibility state + * @property {() => void} onToggleVisibility - Callback to toggle visibility + * @property {boolean} isLocked - Controls the row's locked state + * @property {() => void} onToggleLocked - Callback to toggle locked state + * @property {() => void} onRename - Callback when rename is requested + * @property {() => void} onDelete - Callback when delete is requested + * @property {() => void} onColor - Callback when color change is requested + */ interface DataRowProps { number: number; - title: string; + disableEditing: boolean; description: string; - optionalField?: string; - colorHex?: string; - details?: string; - series?: string; - actionOptions: string[]; - onAction: (action: string) => void; + details?: { primary: string[]; secondary: string[] }; + // isSelected?: boolean; onSelect?: () => void; + // + isVisible: boolean; + onToggleVisibility: () => void; + // + isLocked: boolean; + onToggleLocked: () => void; + // + title: string; + onRename: () => void; + // + onDelete: () => void; + // + colorHex?: string; + onColor: () => void; } const DataRow: React.FC = ({ number, title, - description, - optionalField, colorHex, details, - series, - actionOptions, - onAction, - isSelected = false, onSelect, + isLocked, + onToggleVisibility, + onToggleLocked, + onRename, + onDelete, + onColor, + isSelected = false, + isVisible = true, + disableEditing = false, }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [isVisible, setIsVisible] = useState(true); - const isTitleLong = title.length > 25; + const isTitleLong = title?.length > 25; - return ( - - {' '} - {/* Ensure TooltipProvider wraps the component */} -
- {/* Row 1 with 'group' class to enable group-hover */} + const handleAction = (action: string, e: React.MouseEvent) => { + e.stopPropagation(); + switch (action) { + case 'Rename': + onRename(); + break; + case 'Lock': + onToggleLocked(); + break; + case 'Delete': + onDelete(); + break; + case 'Color': + onColor(); + break; + } + }; + + const decodeHTML = (html: string) => { + const txt = document.createElement('textarea'); + txt.innerHTML = html; + return txt.value; + }; + + const renderDetailText = (text: string, indent: number = 0) => { + const indentation = ' '.repeat(indent); + if (text === '') { + return (
- {/* Hover Overlay */} -
- - {/* Number Box */} -
- {number} -
- - {/* Color Circle (Optional) */} - {colorHex && ( -
- -
- )} - - {/* Label with Conditional Tooltip */} -
- {isTitleLong ? ( - - - - {title} - - - - {title} - - - ) : ( - - {title} - - )} -
- - {/* Actions and Visibility Toggle */} -
- {/* Visibility Toggle Icon */} - {isVisible ? ( - - ) : ( - - )} - - {/* Actions Dropdown Menu */} - setIsDropdownOpen(open)}> - - - - - - - Rename - - - - Lock - - - - Change Color - - - - Delete - - - -
-
- - {/* Row 2 (Details and Series) */} - {(details || series) && ( -
- {/* Updated Flex Container: Changed 'items-center' to 'items-start' */} -
- {details} - {series && {series}} -
-
+ key={`empty-${indent}`} + className="h-2" + >
+ ); // Empty row + } + const cleanText = decodeHTML(text); + return ( +
+ {indentation} + {cleanText.includes(':') ? ( + <> + {sanitizeHtml(cleanText.split(':')[0])}: + {sanitizeHtml(cleanText.split(':')[1])} + + ) : ( + {sanitizeHtml(cleanText)} )}
-
+ ); + }; + + const renderDetails = (details: string[]) => { + const visibleLines = details.slice(0, 4); + const hiddenLines = details.slice(4); + + return ( + + +
+
+ {visibleLines.map((line, lineIndex) => + renderDetailText(line, line.startsWith(' ') ? 1 : 0) + )} +
+ {hiddenLines.length > 0 && ( +
+ ... + +
+ )} +
+
+ +
+ {details.map((line, lineIndex) => + renderDetailText(line, line.startsWith(' ') ? 1 : 0) + )} +
+
+
+ ); + }; + + return ( +
+
+ {/* Hover Overlay */} +
+ + {/* Number Box */} +
+ {number} +
+ + {/* Color Circle (Optional) */} + {colorHex && ( +
+ +
+ )} + + {/* Label with Conditional Tooltip */} +
+ {isTitleLong ? ( + + + + {title} + + + + {title} + + + ) : ( + + {title} + + )} +
+ + {/* Actions and Visibility Toggle */} +
+ {/* Visibility Toggle Icon */} + + + {/* Lock Icon (if needed) */} + {isLocked && } + + {/* Actions Dropdown Menu */} + setIsDropdownOpen(open)}> + + + + + {!disableEditing && ( + <> + handleAction('Rename', e)}> + + Rename + + handleAction('Delete', e)}> + + Delete + + {onColor && ( + handleAction('Color', e)}> + + Change Color + + )} + + )} + handleAction('Lock', e)}> + + {isLocked ? 'Unlock' : 'Lock'} + + + +
+
+ + {details && details.primary?.length > 0 && ( +
+
+ {renderDetails(details.primary)} +
+
+ )} +
); }; diff --git a/platform/ui-next/src/components/DataRow/index.ts b/platform/ui-next/src/components/DataRow/index.ts index 76a817a81..1f3799b2f 100644 --- a/platform/ui-next/src/components/DataRow/index.ts +++ b/platform/ui-next/src/components/DataRow/index.ts @@ -1 +1,3 @@ -export { default as DataRow } from './DataRow'; +import DataRow from './DataRow'; + +export { DataRow }; diff --git a/platform/ui-next/src/components/DataRow/types.ts b/platform/ui-next/src/components/DataRow/types.ts new file mode 100644 index 000000000..60be94201 --- /dev/null +++ b/platform/ui-next/src/components/DataRow/types.ts @@ -0,0 +1,31 @@ +/** + * Represents a single data item in a list or table structure + * + * @interface DataItem + */ +export type DataItem = { + /** Unique identifier for the data item */ + id: number; + /** Primary text or name of the data item */ + title: string; + /** Detailed text description of the data item */ + description: string; + /** Additional optional field for extra information */ + optionalField?: string; + /** Hex color code (e.g., '#FF0000') for visual representation */ + colorHex?: string; + /** Additional details or metadata about the item */ + details?: string; +}; + +/** + * Represents a group of related data items with a common type + * + * @interface ListGroup + */ +export type ListGroup = { + /** The type or category of the group */ + type: string; + /** Array of DataItem objects belonging to this group */ + items: DataItem[]; +}; diff --git a/platform/ui-next/src/components/Dialog/index.ts b/platform/ui-next/src/components/Dialog/index.ts index d792f78d7..ee5229ddf 100644 --- a/platform/ui-next/src/components/Dialog/index.ts +++ b/platform/ui-next/src/components/Dialog/index.ts @@ -1,4 +1,15 @@ -import { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from './Dialog'; +import { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} from './Dialog'; export { Dialog, diff --git a/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx b/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx index 4d59d78cd..48ec78c38 100644 --- a/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx +++ b/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Icons } from '../Icons'; import { useTranslation } from 'react-i18next'; -import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../Tooltip'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip'; /** * Displays a tooltip with a list of messages of a displaySet @@ -14,44 +14,46 @@ const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => { if (messages?.size()) { return ( <> - - - - - - -
-
- {t('Display Set Messages')} -
-
    - {messages.messages.map((message, index) => ( -
  1. - {index + 1}. {t(message.id)} -
  2. - ))} -
+ + + + +
+
+ {t('Display Set Messages')}
- - - +
    + {messages.messages.map((message, index) => ( +
  1. + {index + 1}. {t(message.id)} +
  2. + ))} +
+
+
+
); } diff --git a/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx new file mode 100644 index 000000000..867962c22 --- /dev/null +++ b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; + +import { cn } from '../../lib/utils'; +import { Input } from '../Input'; + +interface DoubleSliderProps { + className?: string; + min: number; + max: number; + step?: number; + defaultValue?: [number, number]; + onValueChange?: (value: [number, number]) => void; +} + +const DoubleSlider = React.forwardRef( + ({ className, min, max, step = 1, defaultValue = [min, max], onValueChange }, ref) => { + const [value, setValue] = React.useState<[number, number]>(defaultValue); + + const prevDefaultValueRef = React.useRef<[number, number] | null>(null); + + const isInteger = step % 1 === 0; + + React.useEffect(() => { + // Only update if defaultValue has actually changed + if ( + !prevDefaultValueRef.current || + prevDefaultValueRef.current[0] !== defaultValue[0] || + prevDefaultValueRef.current[1] !== defaultValue[1] + ) { + setValue(defaultValue); + prevDefaultValueRef.current = defaultValue; + } + }, [defaultValue]); + + const roundToStep = (num: number): number => { + const inverse = 1 / step; + return Math.round(num * inverse) / inverse; + }; + + const handleSliderChange = React.useCallback( + (newValue: number[]) => { + const clampedValue: [number, number] = [ + roundToStep(Math.max(min, Math.min(newValue[0], max))), + roundToStep(Math.min(max, Math.max(newValue[1], min))), + ]; + setValue(clampedValue); + onValueChange?.(clampedValue); + }, + [min, max, onValueChange, step] + ); + + const handleInputChange = React.useCallback( + (index: 0 | 1, inputValue: string) => { + const newValue = parseFloat(inputValue); + if (!isNaN(newValue)) { + const clampedValue: [number, number] = [...value]; + clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max)); + if (index === 0 && clampedValue[0] > clampedValue[1]) { + clampedValue[1] = clampedValue[0]; + } else if (index === 1 && clampedValue[1] < clampedValue[0]) { + clampedValue[0] = clampedValue[1]; + } + setValue(clampedValue); + onValueChange?.(clampedValue); + } + }, + [value, min, max, onValueChange, step] + ); + + const formatValue = (val: number) => { + return isInteger ? Math.round(val) : val; + }; + + return ( +
+ handleInputChange(0, e.target.value)} + onBlur={() => handleInputChange(0, value[0].toString())} + className="w-14" + min={min} + max={max} + step={step} + /> + + + + + + + + handleInputChange(1, e.target.value)} + onBlur={() => handleInputChange(1, value[1].toString())} + className="w-14" + min={min} + max={max} + step={step} + /> +
+ ); + } +); +DoubleSlider.displayName = 'DoubleSlider'; + +export { DoubleSlider }; diff --git a/platform/ui-next/src/components/DoubleSlider/index.tsx b/platform/ui-next/src/components/DoubleSlider/index.tsx new file mode 100644 index 000000000..1c4d718b1 --- /dev/null +++ b/platform/ui-next/src/components/DoubleSlider/index.tsx @@ -0,0 +1,3 @@ +import { DoubleSlider } from './DoubleSlider'; + +export { DoubleSlider }; diff --git a/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx b/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx index 377c18d90..4360ed119 100644 --- a/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx +++ b/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx @@ -20,13 +20,15 @@ const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean; + disabled?: boolean; } ->(({ className, inset, children, ...props }, ref) => ( +>(({ className, inset, children, disabled, ...props }, ref) => ( void; +} + +interface ErrorBoundaryProps { + context?: string; + onReset?: () => void; + onError?: (error: ErrorBoundaryError, componentStack: string, context: string) => void; + fallbackComponent?: React.ComponentType; + children: React.ReactNode; + fallbackRoute?: string | null; + isPage?: boolean; +} + +const DefaultFallback = ({ + error, + context, + resetErrorBoundary = () => {}, +}: DefaultFallbackProps) => { + const { t } = useTranslation('ErrorBoundary'); + const [showDetails, setShowDetails] = useState(false); + const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`; + const subtitle = t('Sorry, something went wrong there. Try again.'); + + const copyErrorDetails = () => { + const errorDetails = ` +Context: ${context} +Error Message: ${error.message} +Stack: ${error.stack} + `; + navigator.clipboard.writeText(errorDetails); + toast.success(t('Copied to clipboard')); + }; + + useEffect(() => { + toast.error(title, { + description: subtitle, + action: { + label: t('Show Details'), + onClick: () => setShowDetails(true), + }, + duration: 0, + }); + }, [error]); + + if (isProduction) { + return null; + } + + return ( + <> + + + + {title} + {subtitle} + + + +
+
+ +
+ +
+

+ {t('Context')}: {context} +

+

+ {t('Error Message')}: {error.message} +

+
+                  Stack: {error.stack}
+                
+
+
+
+
+
+ + ); +}; + +const ErrorBoundary = ({ + context = 'OHIF', + onReset = () => {}, + onError = () => {}, + fallbackComponent: FallbackComponent = DefaultFallback, + children, + fallbackRoute = null, + isPage, +}: ErrorBoundaryProps) => { + const [error, setError] = useState(null); + + const onResetHandler = () => { + setError(null); + onReset(); + }; + + // Add error event listener to window + useEffect(() => { + let errorTimeout: NodeJS.Timeout; + + const handleError = (event: ErrorEvent) => { + event.preventDefault(); + clearTimeout(errorTimeout); + errorTimeout = setTimeout(() => { + setError(event.error); + onErrorHandler(event.error, null); + }, 100); + }; + + const handleRejection = (event: PromiseRejectionEvent) => { + event.preventDefault(); + clearTimeout(errorTimeout); + errorTimeout = setTimeout(() => { + setError(event.reason); + onErrorHandler(event.reason, null); + }, 100); + }; + + window.addEventListener('error', handleError); + window.addEventListener('unhandledrejection', handleRejection); + + return () => { + clearTimeout(errorTimeout); + window.removeEventListener('error', handleError); + window.removeEventListener('unhandledrejection', handleRejection); + }; + }, []); + + const onErrorHandler = (error: ErrorBoundaryError, componentStack: string) => { + console.debug(`${context} Error Boundary`, error, componentStack, context); + onError(error, componentStack, context); + }; + + return ( + ( + + )} + onReset={onResetHandler} + onError={onErrorHandler} + > + <> + {children} + {error && ( + setError(null)} + /> + )} + + + ); +}; + +export { ErrorBoundary }; diff --git a/platform/ui-next/src/components/Errorboundary/index.tsx b/platform/ui-next/src/components/Errorboundary/index.tsx new file mode 100644 index 000000000..31fce4016 --- /dev/null +++ b/platform/ui-next/src/components/Errorboundary/index.tsx @@ -0,0 +1,3 @@ +import { ErrorBoundary } from './ErrorBoundary'; + +export { ErrorBoundary }; diff --git a/platform/ui-next/src/components/Header/Header.tsx b/platform/ui-next/src/components/Header/Header.tsx new file mode 100644 index 000000000..1a6215aa1 --- /dev/null +++ b/platform/ui-next/src/components/Header/Header.tsx @@ -0,0 +1,121 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + Icons, + Button, +} from '../'; + +import NavBar from '../NavBar'; + +// Todo: we should move this component to composition and remove props base + +interface HeaderProps { + children?: ReactNode; + menuOptions: Array<{ + title: string; + icon?: string; + onClick: () => void; + }>; + isReturnEnabled?: boolean; + onClickReturnButton?: () => void; + isSticky?: boolean; + WhiteLabeling?: { + createLogoComponentFn?: (React: any, props: any) => ReactNode; + }; + PatientInfo?: ReactNode; + Secondary?: ReactNode; +} + +function Header({ + children, + menuOptions, + isReturnEnabled = true, + onClickReturnButton, + isSticky = false, + WhiteLabeling, + PatientInfo, + Secondary, + ...props +}: HeaderProps): ReactNode { + const { t } = useTranslation('Header'); + + const onClickReturn = () => { + if (isReturnEnabled && onClickReturnButton) { + onClickReturnButton(); + } + }; + + return ( + +
+
+
+ {isReturnEnabled && } +
+ {WhiteLabeling?.createLogoComponentFn?.(React, props) || } +
+
+
+
{Secondary}
+
+
{children}
+
+
+ {PatientInfo} +
+
+ + + + + + {menuOptions.map((option, index) => { + const IconComponent = option.icon + ? Icons[option.icon as keyof typeof Icons] + : null; + return ( + + {IconComponent && ( + + + + )} + {option.title} + + ); + })} + + +
+
+
+
+ ); +} + +export default Header; diff --git a/platform/ui-next/src/components/Header/index.js b/platform/ui-next/src/components/Header/index.js new file mode 100644 index 000000000..6a0251df9 --- /dev/null +++ b/platform/ui-next/src/components/Header/index.js @@ -0,0 +1,2 @@ +import Header from './Header'; +export { Header }; diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx index 6dad28c6f..4b51892bf 100644 --- a/platform/ui-next/src/components/Icons/Icons.tsx +++ b/platform/ui-next/src/components/Icons/Icons.tsx @@ -1324,6 +1324,7 @@ export const Icons = { viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" + {...props} > status-warning ), + Refresh: (props: IconProps) => ( + + + + + + + + ), Tab4D: (props: IconProps) => ( ), + Plus: (props: IconProps) => ( + + + + + + + + + ), + Minus: (props: IconProps) => ( + + + + + + + ), + + FillAndOutline: (props: IconProps) => ( + + + + + + + + + + ), + OutlineOnly: (props: IconProps) => ( + + + + + + + + + ), + FillOnly: (props: IconProps) => ( + + + + + + + + + ), + EyeVisible: (props: IconProps) => ( + + + + + + + ), + EyeHidden: (props: IconProps) => ( + + + + + + + + + ), + ViewportViews: (props: IconProps) => ( + + + + + + + + ), + StatusChecked: (props: IconProps) => ( + + + + + + + + ), DicomTagBrowser: (props: IconProps) => ( tool-dicom-tag-browser ), + PowerOff: (props: IconProps) => ( + + Power Off + + + ), + MultiplePatients: (props: IconProps) => ( + + icon-multiple-patients + + + + + + + + + + + + + + + + + + + ), + Patient: (props: IconProps) => ( + + icon-patient + + + + + + + + + + + ), + ChevronPatient: (props: IconProps) => ( + + + + + + + + + + + ), + OHIFLogo: (props: IconProps) => ( + + + + + + + + + + + + + + + + + ), + GearSettings: (props: IconProps) => ( + + + + + + + + + + + + ), // Aliases 'tab-segmentation': (props: IconProps) => Icons.TabSegmentation(props), @@ -2211,4 +2860,10 @@ export const Icons = { 'tab-patient-info': (props: IconProps) => Icons.TabPatientInfo(props), 'tab-roi-threshold': (props: IconProps) => Icons.TabRoiThreshold(props), 'icon-mpr': (props: IconProps) => Icons.IconMPR(props), + 'power-off': (props: IconProps) => Icons.PowerOff(props), + 'icon-multiple-patients': (props: IconProps) => Icons.MultiplePatients(props), + 'icon-patient': (props: IconProps) => Icons.Patient(props), + 'icon-chevron-patient': (props: IconProps) => Icons.ChevronPatient(props), + info: (props: IconProps) => Icons.Info(props), + settings: (props: IconProps) => Icons.Settings(props), }; diff --git a/platform/ui-next/src/components/Icons/icon-disclosure-close.svg b/platform/ui-next/src/components/Icons/icon-disclosure-close.svg deleted file mode 100644 index dd6078c55..000000000 --- a/platform/ui-next/src/components/Icons/icon-disclosure-close.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - icon-disclosure-close - - - - - - - - - \ No newline at end of file diff --git a/platform/ui-next/src/components/Icons/icon-disclosure-open.svg b/platform/ui-next/src/components/Icons/icon-disclosure-open.svg deleted file mode 100644 index 7dae7c9ec..000000000 --- a/platform/ui-next/src/components/Icons/icon-disclosure-open.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - icon-disclosure-open - - - - - - - - - \ No newline at end of file diff --git a/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx b/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx new file mode 100644 index 000000000..fd371dee8 --- /dev/null +++ b/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { DataRow, PanelSection } from '../../index'; +import { createContext } from '../../lib/createContext'; + +interface MeasurementTableContext { + data?: any[]; + onClick?: (uid: string) => void; + onDelete?: (uid: string) => void; + onToggleVisibility?: (uid: string) => void; + onToggleLocked?: (uid: string) => void; + onRename?: (uid: string) => void; + onColor?: (uid: string) => void; + disableEditing?: boolean; +} + +const [MeasurementTableProvider, useMeasurementTableContext] = + createContext('MeasurementTable', { data: [] }); + +interface MeasurementDataProps extends MeasurementTableContext { + title: string; + children: React.ReactNode; +} + +const MeasurementTable = ({ + data = [], + onClick, + onDelete, + onToggleVisibility, + onToggleLocked, + onRename, + onColor, + title, + children, + disableEditing = false, +}: MeasurementDataProps) => { + const { t } = useTranslation('MeasurementTable'); + const amount = data.length; + + return ( + + + + {`${t(title)} (${amount})`} + + {children} + + + ); +}; + +const Header = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +const Body = () => { + const { data } = useMeasurementTableContext('MeasurementTable.Body'); + + if (!data || data.length === 0) { + return ( +
+ No tracked measurements +
+ ); + } + + return ( +
+ {data.map((item, index) => ( + + ))} +
+ ); +}; + +const Footer = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +interface MeasurementItem { + uid: string; + label: string; + colorHex: string; + isSelected: boolean; + displayText: { primary: string[]; secondary: string[] }; + isVisible: boolean; + isLocked: boolean; + toolName: string; +} + +interface RowProps { + item: MeasurementItem; + index: number; +} + +const Row = ({ item, index }: RowProps) => { + const { + onClick, + onDelete, + onToggleVisibility, + onToggleLocked, + onRename, + onColor, + disableEditing, + } = useMeasurementTableContext('MeasurementTable.Row'); + + return ( + onClick(item.uid)} + onDelete={() => onDelete(item.uid)} + disableEditing={disableEditing} + isVisible={item.isVisible} + isLocked={item.isLocked} + onToggleVisibility={() => onToggleVisibility(item.uid)} + onToggleLocked={() => onToggleLocked(item.uid)} + onRename={() => onRename(item.uid)} + // onColor={() => onColor(item.uid)} + /> + ); +}; + +MeasurementTable.Header = Header; +MeasurementTable.Body = Body; +MeasurementTable.Footer = Footer; +MeasurementTable.Row = Row; + +export default MeasurementTable; diff --git a/platform/ui-next/src/components/MeasurementTable/index.js b/platform/ui-next/src/components/MeasurementTable/index.js new file mode 100644 index 000000000..42f9beb5c --- /dev/null +++ b/platform/ui-next/src/components/MeasurementTable/index.js @@ -0,0 +1,3 @@ +import MeasurementTable from './MeasurementTable'; + +export { MeasurementTable }; diff --git a/platform/ui-next/src/components/NavBar/NavBar.tsx b/platform/ui-next/src/components/NavBar/NavBar.tsx new file mode 100644 index 000000000..e1a363694 --- /dev/null +++ b/platform/ui-next/src/components/NavBar/NavBar.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +const stickyClasses = 'sticky top-0'; +const notStickyClasses = 'relative'; + +const NavBar = ({ + className, + children, + isSticky, +}: { + className?: string; + children?: React.ReactNode; + isSticky?: boolean; +}) => { + return ( +
+ {children} +
+ ); +}; + +NavBar.propTypes = { + className: PropTypes.string, + children: PropTypes.node, + isSticky: PropTypes.bool, +}; + +export default NavBar; diff --git a/platform/ui-next/src/components/NavBar/index.js b/platform/ui-next/src/components/NavBar/index.js new file mode 100644 index 000000000..35fb05453 --- /dev/null +++ b/platform/ui-next/src/components/NavBar/index.js @@ -0,0 +1,2 @@ +import NavBar from './NavBar'; +export default NavBar; diff --git a/platform/ui-next/src/components/PanelSection/PanelSection.tsx b/platform/ui-next/src/components/PanelSection/PanelSection.tsx index 2c942fe2d..b37c3a54d 100644 --- a/platform/ui-next/src/components/PanelSection/PanelSection.tsx +++ b/platform/ui-next/src/components/PanelSection/PanelSection.tsx @@ -1,66 +1,69 @@ -import React, { useState } from 'react'; -import { Icons } from '../Icons'; -import PropTypes from 'prop-types'; +import React from 'react'; +import { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from '../Accordion/Accordion'; +import { cn } from '../../lib/utils'; +import { Icons } from '../Icons/Icons'; -const PanelSection = ({ title, children, actionIcons = [], childrenClassName }) => { - const [areChildrenVisible, setChildrenVisible] = useState(true); +interface PanelSectionProps { + children: React.ReactNode; + defaultOpen?: boolean; + className?: string; +} - const handleHeaderClick = () => { - setChildrenVisible(!areChildrenVisible); - }; +interface PanelSectionHeaderProps { + children: React.ReactNode; + className?: string; + showChevron?: boolean; +} +interface PanelSectionContentProps { + children: React.ReactNode; + className?: string; +} + +export const PanelSection: React.FC & { + Header: React.FC; + Content: React.FC; +} = ({ children, defaultOpen = true, className }) => { return ( - <> -
+ -
{title}
-
- {actionIcons.map((icon, index) => { - const Icon = Icons[icon.name]; - return ( - { - e.stopPropagation(); - if (!areChildrenVisible) { - setChildrenVisible(true); - } - icon.onClick(); - }} - /> - ); - })} -
- {areChildrenVisible ? : } -
-
-
- {areChildrenVisible && ( - <> -
-
- {children} -
- - )} - + {children} + + ); }; -PanelSection.propTypes = { - title: PropTypes.string, - children: PropTypes.node, - childrenClassName: PropTypes.string, - actionIcons: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - onClick: PropTypes.func, - }) - ), -}; +PanelSection.Header = ({ children, className, showChevron = true }) => ( + + {children} + +); -export { PanelSection }; +PanelSection.Header.displayName = 'PanelSection.Header'; + +PanelSection.Content = ({ children, className }) => ( + +
{children}
+
+); + +PanelSection.Content.displayName = 'PanelSection.Content'; diff --git a/platform/ui-next/src/components/Popover/Popover.tsx b/platform/ui-next/src/components/Popover/Popover.tsx index bdd335d79..bd5ffcb8d 100644 --- a/platform/ui-next/src/components/Popover/Popover.tsx +++ b/platform/ui-next/src/components/Popover/Popover.tsx @@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( - 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-input z-50 w-72 rounded-md border p-4 shadow-md outline-none', className )} {...props} diff --git a/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx b/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx index ba9e3bcac..4f0a304ab 100644 --- a/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx +++ b/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx @@ -2,25 +2,101 @@ import * as React from 'react'; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import { cn } from '../../lib/utils'; +import { Icons } from '../Icons'; +/** + * Props interface for the ScrollArea component. + * Extends Radix UI ScrollArea root props. + */ +interface ScrollAreaProps extends React.ComponentPropsWithoutRef { + /** Flag to show/hide scroll indicator arrows at top and bottom */ + showArrows?: boolean; +} + +/** + * A custom scroll area component built on top of Radix UI's ScrollArea. + * Provides a scrollable container with custom styling and optional scroll indicators. + * + * @param props - The component props + * @param props.className - Additional CSS classes to apply + * @param props.children - The content to be scrolled + * @param props.showArrows - Whether to show scroll indicator arrows + * @param ref - Forward ref for the root element + * + * @example + * ```tsx + * + *
Scrollable content
+ *
+ * ``` + */ const ScrollArea = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)); + ScrollAreaProps +>(({ className, children, showArrows = false, ...props }, ref) => { + const [showBottomArrow, setShowBottomArrow] = React.useState(false); + const [showTopArrow, setShowTopArrow] = React.useState(false); + const viewportRef = React.useRef(null); + + const checkScroll = React.useCallback(() => { + if (viewportRef.current) { + const { scrollHeight, clientHeight, scrollTop } = viewportRef.current; + setShowBottomArrow(scrollHeight > clientHeight && scrollTop < scrollHeight - clientHeight); + setShowTopArrow(scrollTop > 0); + } + }, []); + + React.useEffect(() => { + checkScroll(); + window.addEventListener('resize', checkScroll); + return () => window.removeEventListener('resize', checkScroll); + }, [checkScroll]); + + return ( + + + {children} + + + + {showArrows && showTopArrow && ( +
+ +
+ )} + {showArrows && showBottomArrow && ( +
+ +
+ )} +
+ ); +}); + ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; +/** + * Custom scrollbar component for the ScrollArea. + * Provides styled scrollbars that can be either vertical or horizontal. + * + * @param props - The component props + * @param props.className - Additional CSS classes to apply + * @param props.orientation - The scrollbar orientation ('vertical' | 'horizontal') + * @param ref - Forward ref for the scrollbar element + * + * @example + * ```tsx + * + * ``` + */ const ScrollBar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx new file mode 100644 index 000000000..5bf9120a8 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Button, Icons } from '@ohif/ui-next'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +/** + * Props interface for the AddSegmentRow component + */ +interface AddSegmentRowProps { + /** Optional child elements to render within the row */ + children?: React.ReactNode; + /** Optional segmentation object to override the active segmentation */ + segmentation?: unknown; +} + +/** + * A component that renders a row with controls for adding segments and toggling visibility + * in the segmentation table. + * + * @param props - Component properties + * @param props.children - Optional child elements to render within the row + * @param props.segmentation - Optional segmentation object to override the active segmentation + */ +export const AddSegmentRow: React.FC = ({ children = null, segmentation }) => { + const { + activeRepresentation, + disableEditing, + activeSegmentationId, + onSegmentAdd, + onToggleSegmentationRepresentationVisibility, + data, + showAddSegment, + } = useSegmentationTableContext('SegmentationTable'); + + const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every( + segment => segment?.visible !== false + ); + + const segmentationIdToUse = segmentation ? segmentation.segmentationId : activeSegmentationId; + + if (!data?.length) { + return null; + } + + const Icon = allSegmentsVisible ? ( + + ) : ( + + ); + + const allowAddSegment = showAddSegment && !disableEditing; + + return ( +
+ {allowAddSegment ? ( + + ) : null} + + {children} +
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/AddSegmentationRow.tsx b/platform/ui-next/src/components/SegmentationTable/AddSegmentationRow.tsx new file mode 100644 index 000000000..2bc56cb79 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/AddSegmentationRow.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Icons } from '../Icons'; +import { useTranslation } from 'react-i18next'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({ + children = null, +}) => { + const { t } = useTranslation('SegmentationTable'); + + const { onSegmentationAdd, data, disableEditing, mode, disabled } = + useSegmentationTableContext('SegmentationTable'); + + const isEmpty = data.length === 0; + + if (!isEmpty && mode === 'collapsed') { + return null; + } + + if (disableEditing) { + return null; + } + + return ( +
!disabled && onSegmentationAdd('')} + > + {children} +
+
+ {disabled ? : } +
+ + {t(`${disabled ? 'Segmentation Not Supported' : 'Add Segmentation'}`)} + +
+
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationCollapsed.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationCollapsed.tsx new file mode 100644 index 000000000..bb139dc92 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationCollapsed.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const SegmentationCollapsed: React.FC<{ children?: React.ReactNode }> = ({ + children = null, +}) => { + return
{children}
; +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx new file mode 100644 index 000000000..02b0cc728 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useSegmentationTableContext } from './SegmentationTableContext'; +import { PanelSection } from '../PanelSection'; +import { SegmentationHeader } from './SegmentationHeader'; +import { SegmentationTable } from './SegmentationTable'; + +export const SegmentationExpanded: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const { data } = useSegmentationTableContext('SegmentationExpanded'); + + // Separate the Header component from other children + const headerComponent = React.Children.toArray(children).find( + child => React.isValidElement(child) && child.type === SegmentationTable.Header + ); + const otherChildren = React.Children.toArray(children).filter( + child => !(React.isValidElement(child) && child.type === SegmentationTable.Header) + ); + + return ( + <> + {data.map(segmentationInfo => ( + + + {headerComponent ? ( + React.cloneElement(headerComponent as React.ReactElement, { + segmentation: segmentationInfo.segmentation, + representation: segmentationInfo.representation, + }) + ) : ( + + )} + + +
+ {React.Children.map(otherChildren, child => + React.isValidElement(child) + ? React.cloneElement(child, { + segmentation: segmentationInfo.segmentation, + representation: segmentationInfo.representation, + }) + : child + )} +
+
+
+ ))} + + ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx new file mode 100644 index 000000000..8cb6d4932 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { Button } from '../Button'; +import { Icons } from '../Icons/Icons'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuPortal, +} from '../DropdownMenu'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip'; +import { useSegmentationTableContext } from './SegmentationTableContext'; +import { useTranslation } from 'react-i18next'; + +export const SegmentationHeader: React.FC<{ + segmentation?: any; +}> = ({ segmentation }) => { + const { t } = useTranslation('SegmentationTable'); + const { + onSegmentAdd, + onSegmentationRemoveFromViewport, + onSegmentationEdit, + onSegmentationDelete, + onSegmentationDownload, + onSegmentationDownloadRTSS, + storeSegmentation, + } = useSegmentationTableContext('SegmentationHeader'); + + if (!segmentation) { + return null; + } + + return ( +
+
+ + + + + + { + e.stopPropagation(); + onSegmentAdd(segmentation.segmentationId); + }} + > + + {t('Add Segment')} + + + { + e.stopPropagation(); + onSegmentationRemoveFromViewport(segmentation.segmentationId); + }} + > + + {t('Remove from Viewport')} + + { + e.stopPropagation(); + onSegmentationEdit(segmentation.segmentationId); + }} + > + + {t('Rename')} + + e.stopPropagation()}> + + {t('Hide or Show all Segments')} + + + e.stopPropagation()}> + + {t('Export & Download')} + + + + { + e.stopPropagation(); + storeSegmentation(segmentation.segmentationId); + }} + > + {t('Export DICOM SEG')} + + { + e.stopPropagation(); + onSegmentationDownload(segmentation.segmentationId); + }} + > + {t('Download DICOM SEG')} + + { + e.stopPropagation(); + onSegmentationDownloadRTSS(segmentation.segmentationId); + }} + > + {t('Download DICOM RTSTRUCT')} + + + + + + onSegmentationDelete(segmentation.segmentationId)}> + + {t('Delete')} + + + +
{segmentation.label}
+
+
+ + + + + +

{segmentation.cachedStats.info}

+
+
+
+
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx new file mode 100644 index 000000000..98b8f016b --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { ScrollArea, DataRow } from '../../components'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const SegmentationSegments: React.FC<{ + segmentation?: unknown; + representation?: unknown; +}> = ({ segmentation, representation }) => { + const { + activeSegmentationId, + disableEditing, + onSegmentColorClick, + onToggleSegmentVisibility, + onToggleSegmentLock, + onSegmentClick, + mode, + onSegmentEdit, + onSegmentDelete, + data, + } = useSegmentationTableContext('SegmentationTable.Segments'); + + let segmentationToUse = segmentation; + let representationToUse = representation; + let segmentationIdToUse = activeSegmentationId; + if (!segmentationToUse || !representationToUse) { + const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId); + segmentationToUse = entry?.segmentation; + representationToUse = entry?.representation; + segmentationIdToUse = entry?.segmentation.segmentationId; + } + + if (!representationToUse || !segmentationToUse) { + return null; + } + + const segmentCount = Object.keys(representationToUse.segments).length; + const height = mode === 'collapsed' ? 'h-[600px]' : `h-[${segmentCount * 200}px]`; + + return ( + + {Object.values(representationToUse.segments).map(segment => { + if (!segment) { + return null; + } + const { segmentIndex, color, visible } = segment; + const segmentFromSegmentation = segmentationToUse.segments[segmentIndex]; + + if (!segmentFromSegmentation) { + return null; + } + + const { locked, active, label, displayText } = segmentFromSegmentation; + const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`; + + return ( + onSegmentColorClick(segmentationIdToUse, segmentIndex)} + onToggleVisibility={() => + onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type) + } + onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)} + onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)} + onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)} + onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)} + /> + ); + })} + + ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx new file mode 100644 index 000000000..720e4864c --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { + Icons, + Button, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuPortal, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + Tooltip, + TooltipTrigger, + TooltipContent, + DropdownMenuLabel, +} from '../../components'; + +import { useTranslation } from 'react-i18next'; +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@ohif/ui-next'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }> = ({ + children = null, +}) => { + const { t } = useTranslation('SegmentationTable.HeaderCollapsed'); + + const { + data, + activeSegmentationId, + mode, + onSegmentationClick, + onSegmentationAdd, + onSegmentationRemoveFromViewport, + onSegmentationEdit, + onSegmentationDelete, + onSegmentationDownload, + onSegmentationDownloadRTSS, + storeSegmentation, + exportOptions, + } = useSegmentationTableContext('SegmentationTable.HeaderCollapsed'); + + if (mode !== 'collapsed' || !data?.length) { + return null; + } + + const activeSegmentationObj = data.find( + seg => seg.segmentation.segmentationId === activeSegmentationId + ); + + const activeSegmentation = { + id: activeSegmentationObj?.segmentation.segmentationId, + label: activeSegmentationObj?.segmentation.label, + info: activeSegmentationObj?.segmentation.cachedStats?.info, + }; + + const segmentations = data.map(seg => ({ + id: seg.segmentation.segmentationId, + label: seg.segmentation.label, + info: seg.segmentation.cachedStats?.info, + })); + + const allowExport = exportOptions.find( + ({ segmentationId }) => segmentationId === activeSegmentation.id + )?.isExportable; + + return ( +
+ + + + + + onSegmentationAdd(activeSegmentation.id)}> + + {t('Create New Segmentation')} + + + {t('Manage Current Segmentation')} + onSegmentationRemoveFromViewport(activeSegmentation.id)}> + + {t('Remove from Viewport')} + + onSegmentationEdit(activeSegmentation.id)}> + + {t('Rename')} + + + + + {t('Export & Download')} + + + + storeSegmentation(activeSegmentation.id)}> + {t('Export DICOM SEG')} + + onSegmentationDownload(activeSegmentation.id)}> + {t('Download DICOM SEG')} + + onSegmentationDownloadRTSS(activeSegmentation.id)}> + {t('Download DICOM RTSTRUCT')} + + + + + + onSegmentationDelete(activeSegmentation.id)}> + + {t('Delete')} + + + + + + + + + + {activeSegmentation.info} + + +
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx new file mode 100644 index 000000000..4859cfbe2 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx @@ -0,0 +1,76 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PanelSection } from '../PanelSection'; +import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext'; +import { SegmentationSegments } from './SegmentationSegments'; +import { SegmentationTableConfig } from './SegmentationTableConfig'; +import { AddSegmentRow } from './AddSegmentRow'; +import { AddSegmentationRow } from './AddSegmentationRow'; +import { SegmentationSelectorHeader } from './SegmentationSelectorHeader'; +import { SegmentationHeader } from './SegmentationHeader'; +import { SegmentationCollapsed } from './SegmentationCollapsed'; +import { SegmentationExpanded } from './SegmentationExpanded'; + +interface SegmentationTableProps extends SegmentationTableContext { + disabled?: boolean; + title?: string; + children?: ReactNode; +} + +interface SegmentationTableComponent extends React.FC { + Segments: typeof SegmentationSegments; + Config: typeof SegmentationTableConfig; + AddSegmentRow: typeof AddSegmentRow; + AddSegmentationRow: typeof AddSegmentationRow; + SelectorHeader: typeof SegmentationSelectorHeader; + Header: typeof SegmentationHeader; + Collapsed: typeof SegmentationCollapsed; + Expanded: typeof SegmentationExpanded; +} + +export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => { + const { t } = useTranslation('SegmentationTable'); + const { data = [], mode, title, disableEditing, disabled, children, ...contextProps } = props; + + const activeSegmentationInfo = data.find(info => info.representation?.active); + + const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId; + const activeRepresentation = activeSegmentationInfo?.representation; + const activeSegmentation = activeSegmentationInfo?.segmentation; + const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } = + activeRepresentation?.styles ?? {}; + + return ( + + + + {t(title)} + + {children} + + + ); +}; + +SegmentationTable.Segments = SegmentationSegments; +SegmentationTable.Config = SegmentationTableConfig; +SegmentationTable.AddSegmentRow = AddSegmentRow; +SegmentationTable.AddSegmentationRow = AddSegmentationRow; +SegmentationTable.SelectorHeader = SegmentationSelectorHeader; +SegmentationTable.Header = SegmentationHeader; +SegmentationTable.Collapsed = SegmentationCollapsed; +SegmentationTable.Expanded = SegmentationExpanded; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx new file mode 100644 index 000000000..2f5fc220e --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { PanelSection } from '../PanelSection'; +import { Tabs, TabsList, TabsTrigger } from '../Tabs'; +import { Slider } from '../Slider'; +import { Icons } from '../Icons'; +import { Switch } from '../Switch'; +import { Label } from '../Label'; +import { Input } from '../Input'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const { t } = useTranslation('SegmentationTable.AppearanceSettings'); + const { + renderFill, + renderOutline, + setRenderFill, + setRenderOutline, + activeRepresentation, + fillAlpha, + fillAlphaInactive, + outlineWidth, + setFillAlpha, + setFillAlphaInactive, + setOutlineWidth, + renderInactiveSegmentations, + toggleRenderInactiveSegmentations, + data, + } = useSegmentationTableContext('styles'); + + if (!data?.length) { + return null; + } + + return ( + + +
+ + {t('Appearance Settings')} +
+
+ +
+
+ + {t('Show')}:{' '} + {renderFill && renderOutline + ? t('Fill & Outline') + : renderOutline + ? t('Outline Only') + : t('Fill Only')} + + { + if (value === 'fill-and-outline') { + setRenderFill({ type: activeRepresentation.type }, true); + setRenderOutline({ type: activeRepresentation.type }, true); + } else if (value === 'outline') { + setRenderFill({ type: activeRepresentation.type }, false); + setRenderOutline({ type: activeRepresentation.type }, true); + } else { + setRenderFill({ type: activeRepresentation.type }, true); + setRenderOutline({ type: activeRepresentation.type }, false); + } + }} + > + + + + + + + + + + + + +
+ +
+
+ + + setFillAlpha({ type: activeRepresentation.type }, value) + } + max={1} + min={0} + step={0.1} + /> + + setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value)) + } + /> +
+ +
+ + + setOutlineWidth({ type: activeRepresentation.type }, value) + } + max={10} + min={0} + step={0.1} + className="mx-1 flex-1" + /> + + setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value)) + } + className="mx-1 w-10 flex-none text-center" + /> +
+
+ +
+ +
+ + +
+ {renderInactiveSegmentations && ( +
+ + + setFillAlphaInactive({ type: activeRepresentation.type }, value) + } + max={1} + min={0} + step={0.1} + /> + + setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value)) + } + /> +
+ )} +
+ {children} +
+
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts b/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts new file mode 100644 index 000000000..f299b9e05 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts @@ -0,0 +1,80 @@ +import { createContext } from '../../lib/createContext'; + +interface Segmentation { + segmentationId: string; + label: string; + cachedStats: { + info: string; + }; +} + +interface Representation { + active: boolean; + visible: boolean; + type: string; + styles: { + fillAlpha: number; + fillAlphaInactive: number; + outlineWidth: number; + renderFill: boolean; + renderOutline: boolean; + }; +} + +interface ViewportSegmentationInfo { + segmentation: Segmentation; + representation: Representation; +} + +interface SegmentationTableContext { + data: ViewportSegmentationInfo[]; + disabled: boolean; + mode: 'collapsed' | 'expanded'; + fillAlpha: number; + exportOptions: { + segmentationId: string; + isExportable: boolean; + }[]; + fillAlphaInactive: number; + outlineWidth: number; + renderFill: boolean; + renderOutline: boolean; + activeSegmentationId: string; + activeRepresentation: Representation; + activeSegmentation: Segmentation; + setRenderFill: ({ type }, value: boolean) => void; + setRenderOutline: ({ type }, value: boolean) => void; + setOutlineWidth: ({ type }, value: number) => void; + setFillAlpha: ({ type }, value: number) => void; + setFillAlphaInactive: ({ type }, value: number) => void; + renderInactiveSegmentations: boolean; + toggleRenderInactiveSegmentations: () => void; + onSegmentationAdd: (segmentationId: string) => void; + onSegmentationClick: (segmentationId: string) => void; + onSegmentationDelete: (segmentationId: string) => void; + onSegmentAdd: (segmentationId: string) => void; + onSegmentClick: (segmentationId: string, segmentIndex: number) => void; + onSegmentEdit: (segmentationId: string, segmentIndex: number) => void; + onSegmentationEdit: (segmentationId: string) => void; + onSegmentColorClick: (segmentationId: string, segmentIndex: number) => void; + onSegmentDelete: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentVisibility: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentLock: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentationRepresentationVisibility: (segmentationId: string, type: string) => void; + onSegmentationDownload: (segmentationId: string) => void; + storeSegmentation: (segmentationId: string) => void; + onSegmentationDownloadRTSS: (segmentationId: string) => void; + setStyle: ( + segmentationId: string, + representationType: string, + styleKey: string, + value: unknown + ) => void; + onSegmentationRemoveFromViewport: (segmentationId: string) => void; + disableEditing: boolean; +} + +const [SegmentationTableProvider, useSegmentationTableContext] = + createContext('SegmentationTable'); + +export { SegmentationTableProvider, useSegmentationTableContext, SegmentationTableContext }; diff --git a/platform/ui-next/src/components/SegmentationTable/index.ts b/platform/ui-next/src/components/SegmentationTable/index.ts new file mode 100644 index 000000000..b6704de80 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/index.ts @@ -0,0 +1,22 @@ +import { SegmentationTable } from './SegmentationTable'; +import { SegmentationTableConfig } from './SegmentationTableConfig'; +import { AddSegmentationRow } from './AddSegmentationRow'; +import { AddSegmentRow } from './AddSegmentRow'; +import { SegmentationSegments } from './SegmentationSegments'; +import { SegmentationSelectorHeader } from './SegmentationSelectorHeader'; +import { SegmentationHeader } from './SegmentationHeader'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +SegmentationTable.Segments = SegmentationSegments; +SegmentationTable.Config = SegmentationTableConfig; +SegmentationTable.AddSegmentRow = AddSegmentRow; +SegmentationTable.AddSegmentationRow = AddSegmentationRow; +SegmentationTable.SelectorHeader = SegmentationSelectorHeader; +SegmentationTable.Header = SegmentationHeader; + +export { + // context + useSegmentationTableContext, + // components + SegmentationTable, +}; diff --git a/platform/ui-next/src/components/Select/index.tsx b/platform/ui-next/src/components/Select/index.tsx index 431068e34..9610fe2a1 100644 --- a/platform/ui-next/src/components/Select/index.tsx +++ b/platform/ui-next/src/components/Select/index.tsx @@ -23,3 +23,18 @@ export { SelectScrollUpButton, SelectScrollDownButton, }; + +const SelectComponents = { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; + +export default SelectComponents; diff --git a/platform/ui-next/src/components/Separator/Separator.tsx b/platform/ui-next/src/components/Separator/Separator.tsx index c9a836779..7c6958cec 100644 --- a/platform/ui-next/src/components/Separator/Separator.tsx +++ b/platform/ui-next/src/components/Separator/Separator.tsx @@ -1,5 +1,5 @@ -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; import { cn } from '../../lib/utils'; @@ -12,7 +12,7 @@ const Separator = React.forwardRef< SeparatorProps >( ( - { className, orientation = "horizontal", decorative = true, thickness = "1px", ...props }, + { className, orientation = 'horizontal', decorative = true, thickness = '1px', ...props }, ref ) => ( ) -) -Separator.displayName = SeparatorPrimitive.Root.displayName +); +Separator.displayName = SeparatorPrimitive.Root.displayName; -export { Separator } \ No newline at end of file +export { Separator }; diff --git a/platform/ui-next/src/components/SidePanel/SidePanel.tsx b/platform/ui-next/src/components/SidePanel/SidePanel.tsx index 811e77658..bf35d8e93 100644 --- a/platform/ui-next/src/components/SidePanel/SidePanel.tsx +++ b/platform/ui-next/src/components/SidePanel/SidePanel.tsx @@ -212,43 +212,41 @@ const SidePanel = ({ />
- - {_childComponents.map((childComponent, index) => ( - - -
{ - return childComponent.disabled ? null : updateActiveTabIndex(index); - }} - > - {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { - className: classnames({ - 'text-primary-active': true, - 'ohif-disabled': childComponent.disabled, - }), - style: { - width: '22px', - height: '22px', - }, - })} -
-
- -
- {getToolTipContent(childComponent.label, childComponent.disabled)} -
-
-
- ))} -
+ {_childComponents.map((childComponent, index) => ( + + +
{ + return childComponent.disabled ? null : updateActiveTabIndex(index); + }} + > + {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { + className: classnames({ + 'text-primary-active': true, + 'ohif-disabled': childComponent.disabled, + }), + style: { + width: '22px', + height: '22px', + }, + })} +
+
+ +
+ {getToolTipContent(childComponent.label, childComponent.disabled)} +
+
+
+ ))}
); @@ -296,47 +294,42 @@ const SidePanel = ({
)} - - - + + +
{ + return disabled ? null : updateActiveTabIndex(tabIndex); + }} + data-cy={`${tab.name}-btn`} + >
{ - return disabled ? null : updateActiveTabIndex(tabIndex); - }} - data-cy={`${tab.name}-btn`} + className={getTabIconClassNames(tabs.length, tabIndex === activeTabIndex)} > -
- {React.createElement(Icons[tab.iconName] || Icons.MissingIcon, { - className: classnames({ - 'text-primary-active': true, - 'ohif-disabled': disabled, - }), - style: { - width: '22px', - height: '22px', - }, - })} -
+ {React.createElement(Icons[tab.iconName] || Icons.MissingIcon, { + className: classnames({ + 'text-primary-active': true, + 'ohif-disabled': disabled, + }), + style: { + width: '22px', + height: '22px', + }, + })}
- - - {getToolTipContent(tab.label, disabled)} - - - +
+
+ + {getToolTipContent(tab.label, disabled)} + +
); })} diff --git a/platform/ui-next/src/components/Sonner/Sonner.tsx b/platform/ui-next/src/components/Sonner/Sonner.tsx index dd3fab818..368e8edea 100644 --- a/platform/ui-next/src/components/Sonner/Sonner.tsx +++ b/platform/ui-next/src/components/Sonner/Sonner.tsx @@ -18,16 +18,9 @@ const Toaster = ({ ...props }: ToasterProps) => { theme="dark" richColors="true" toastOptions={{ - classNames: { - toast: - 'group toast group-[.toaster]:bg-popover group-[.toaster]:text-foreground group-[.toaster]:border-background group-[.toaster]:shadow-lg', - info: 'group-[.toast] group-[.toaster]:!bg-popover group-[.toaster]:!text-foreground group-[.toaster]:!border-background', - error: 'group-[.toaster]:!border-background', - success: 'group-[.toaster]:!border-background', - warning: 'group-[.toaster]:!border-background', - description: 'group-[.toast]:text-secondary-foreground', - actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground', - cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground', + style: { + width: '430px', // Set a maximum width + right: '8px', }, }} {...props} diff --git a/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx index dba1580b1..fd553dbaf 100644 --- a/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx +++ b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx @@ -70,24 +70,24 @@ const StudyBrowser = ({ }; return ( - +
{showSettings && ( -
- - +
+ <> + + +
)} +
{getTabContent()}
- +
); }; diff --git a/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx b/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx index a5538c2ab..ac000022b 100644 --- a/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx +++ b/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx @@ -1,5 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Icons } from '../Icons'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../DropdownMenu/DropdownMenu'; export function StudyBrowserSort({ servicesManager }: withAppTypes) { const { customizationService, displaySetService } = servicesManager.services; @@ -8,9 +14,8 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) { const [selectedSort, setSelectedSort] = useState(sortFunctions[0]); const [sortDirection, setSortDirection] = useState('ascending'); - const handleSortChange = event => { - const selectedSortFunction = sortFunctions.find(sort => sort.label === event.target.value); - setSelectedSort(selectedSortFunction); + const handleSortChange = sortFunction => { + setSelectedSort(sortFunction); }; const toggleSortDirection = e => { @@ -43,26 +48,26 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) { }, [displaySetService, selectedSort, sortDirection]); return ( -
- +
+ + + {selectedSort.label} + + + {sortFunctions.map(sort => ( + handleSortChange(sort)} + > + {sort.label} + + ))} + + { @@ -162,12 +159,23 @@ const Thumbnail = ({ Tag Browser + {canReject && ( + { + onReject(); + }} + className="gap-[6px]" + > + + Delete Report + + )}
-
+
{description}
@@ -175,7 +183,6 @@ const Thumbnail = ({
S:{seriesNumber}
- {' '} {countIcon ? ( React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' }) ) : ( @@ -192,7 +199,12 @@ const Thumbnail = ({ const renderListPreset = () => { return ( -
+
- {canReject && ( - - )} + {isTracked && ( - - - -
- - + + +
+ + +
+
+ +
+
+
- - -
-
- -
-
- - - {isTracked ? 'Series is tracked' : 'Series is untracked'} - +
+ + + {isTracked ? 'Series is tracked' : 'Series is untracked'} -
+
- - - +
+
+ )} @@ -276,10 +281,7 @@ const Thumbnail = ({ - + { onThumbnailContextMenu('openDICOMTagViewer', { @@ -291,6 +293,17 @@ const Thumbnail = ({ Tag Browser + {canReject && ( + { + onReject(); + }} + className="gap-[6px]" + > + + Delete Report + + )}
diff --git a/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx index 89eed8785..22dbd2c36 100644 --- a/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx +++ b/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx @@ -33,7 +33,6 @@ const ThumbnailList = ({ loadingProgress, modality, componentType, - seriesDate, countIcon, isTracked, canReject, diff --git a/platform/ui-next/src/components/Toolbox/Toolbox.tsx b/platform/ui-next/src/components/Toolbox/Toolbox.tsx index 161ca17e9..5e23fb774 100644 --- a/platform/ui-next/src/components/Toolbox/Toolbox.tsx +++ b/platform/ui-next/src/components/Toolbox/Toolbox.tsx @@ -3,7 +3,6 @@ import { useToolbar } from '@ohif/core'; import { ToolboxUI } from './'; // Migrate this file to the new UI eventually import { useToolbox } from '@ohif/ui'; -import { Separator } from '../Separator'; /** * A toolbox is a collection of buttons and commands that they invoke, used to provide diff --git a/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx b/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx index 00c381b4e..b00079826 100644 --- a/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx +++ b/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx @@ -110,11 +110,11 @@ function ToolboxUI(props: withAppTypes) { return ( <> {useCollapsedPanel ? ( - - {render()} + + + {title} + + {render()} ) : ( render() diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index 43936f812..cb3bf884d 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -50,6 +50,7 @@ import { Thumbnail } from './Thumbnail'; import { ThumbnailList } from './ThumbnailList'; import { PanelSection } from './PanelSection'; import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip'; import { Toolbox, ToolboxUI } from './Toolbox'; import { DropdownMenu, @@ -69,14 +70,21 @@ import { DropdownMenuRadioGroup, } from './DropdownMenu'; import { Onboarding } from './Onboarding'; -import { BackgroundColorSelect } from './BackgroundColorSelect'; +import { DoubleSlider } from './DoubleSlider'; import { DataRow } from './DataRow'; -import { Toaster } from './Sonner'; +import { MeasurementTable } from './MeasurementTable'; +import { SegmentationTable, useSegmentationTableContext } from './SegmentationTable'; +import { Toaster, toast } from './Sonner'; +import { StudySummary } from './StudySummary'; +import { ErrorBoundary } from './Errorboundary'; +import { Header } from './Header'; export { + ErrorBoundary, Button, buttonVariants, ThemeWrapper, + DoubleSlider, Command, CommandDialog, CommandInput, @@ -151,12 +159,21 @@ export { DropdownMenuSubTrigger, DropdownMenuRadioGroup, Onboarding, - BackgroundColorSelect, - DataRow, Select, + SelectTrigger, SelectContent, SelectItem, - SelectTrigger, SelectValue, + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider, + DataRow, + MeasurementTable, Toaster, + toast, + SegmentationTable, + useSegmentationTableContext, + StudySummary, + Header, }; diff --git a/platform/ui-next/src/contextProviders/NotificationProvider.tsx b/platform/ui-next/src/contextProviders/NotificationProvider.tsx index 6cabfa317..09e526245 100644 --- a/platform/ui-next/src/contextProviders/NotificationProvider.tsx +++ b/platform/ui-next/src/contextProviders/NotificationProvider.tsx @@ -1,6 +1,6 @@ -import React, { useState, createContext, useContext, useCallback, useEffect } from 'react'; +import React, { createContext, useContext, useCallback, useEffect, ReactNode } from 'react'; import PropTypes from 'prop-types'; -import { Toaster, toast } from 'sonner'; +import { Toaster, toast } from '../components'; const NotificationContext = createContext(null); @@ -15,38 +15,41 @@ const NotificationProvider = ({ children, service }) => { type: 'info', // info, success, error }; - const [notificationItems, setNotificationItems] = useState([]); - const show = useCallback(options => { - const { title, message, duration, position, type } = { ...DEFAULT_OPTIONS, ...options }; - const id = toast[type](message, { + const { title, message, duration, position, type, promise } = { + ...DEFAULT_OPTIONS, + ...options, + }; + + if (promise) { + return toast.promise(promise, { + loading: title || 'Loading...', + success: (data: unknown) => ({ + title: title || 'Success', + description: typeof message === 'function' ? message(data) : message, + }), + error: (err: unknown) => ({ + title: title || 'Error', + description: typeof message === 'function' ? message(err) : message, + }), + }); + } + + return toast[type](title, { duration, position, - render: () => ( -
- {title} -

{message}

-
- ), + description: message, }); - - setNotificationItems(state => [...state, { id, ...options }]); }, []); const hide = useCallback(id => { toast.dismiss(id); - setNotificationItems(state => state.filter(item => item.id !== id)); }, []); const hideAll = useCallback(() => { toast.dismiss(); - setNotificationItems([]); }, []); - useEffect(() => { - window.notification = { show, hide, hideAll }; - }, [show, hide, hideAll]); - /** * Sets the implementation of a notification service that can be used by extensions. * @@ -59,7 +62,7 @@ const NotificationProvider = ({ children, service }) => { }, [service, hide, show]); return ( - + {children} diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index ee024b0bf..a4b7dab1e 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -1,5 +1,4 @@ import { - BackgroundColorSelect, Button, buttonVariants, ThemeWrapper, @@ -34,7 +33,7 @@ import { DisplaySetMessageListTooltip, Toolbox, ToolboxUI, - ScrollArea, + DoubleSlider, Label, Slider, Input, @@ -49,7 +48,6 @@ import { SelectContent, SelectItem, SelectValue, - DataRow, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, @@ -65,11 +63,24 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, + ScrollArea, + MeasurementTable, + SegmentationTable, + useSegmentationTableContext, + TooltipProvider, + Tooltip, + TooltipTrigger, + TooltipContent, + StudySummary, + ErrorBoundary, + Header, } from './components'; +import { DataRow } from './components/DataRow'; import { useNotification, NotificationProvider } from './contextProviders'; export { + ErrorBoundary, // components Button, Dialog, @@ -77,6 +88,7 @@ export { Popover, Combobox, Checkbox, + DoubleSlider, buttonVariants, ThemeWrapper, Calendar, @@ -109,7 +121,6 @@ export { DisplaySetMessageListTooltip, Toolbox, ToolboxUI, - ScrollArea, Label, Slider, Input, @@ -124,7 +135,6 @@ export { SelectItem, SelectValue, DataRow, - BackgroundColorSelect, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, @@ -140,4 +150,14 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, + ScrollArea, + MeasurementTable, + SegmentationTable, + useSegmentationTableContext, + TooltipProvider, + Tooltip, + TooltipTrigger, + TooltipContent, + StudySummary, + Header, }; diff --git a/platform/ui-next/src/lib/createContext.tsx b/platform/ui-next/src/lib/createContext.tsx new file mode 100644 index 000000000..ef3cb31e0 --- /dev/null +++ b/platform/ui-next/src/lib/createContext.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +// https://github.com/reach/reach-ui/blob/dev/packages/utils/src/context.tsx +type ContextProvider = React.FC>; + +export function createContext( + rootComponentName: string, + defaultContext?: ContextValueType +): [ContextProvider, (callerComponentName: string) => ContextValueType] { + const Ctx = React.createContext(defaultContext); + + function Provider(props: React.PropsWithChildren) { + const { children, ...context } = props; + const value = React.useMemo( + () => context, + // eslint-disable-next-line react-hooks/exhaustive-deps + Object.values(context) + ) as ContextValueType; + return {children}; + } + + function useContext(callerComponentName: string) { + const context = React.useContext(Ctx); + if (context) { + return context; + } + if (defaultContext) { + return defaultContext; + } + throw Error( + `${callerComponentName} must be rendered inside of a ${rootComponentName} component.` + ); + } + + Ctx.displayName = `${rootComponentName}Context`; + Provider.displayName = `${rootComponentName}Provider`; + return [Provider, useContext]; +} diff --git a/platform/ui-next/src/tailwind.css b/platform/ui-next/src/tailwind.css index ec37c3ba7..fc32c0a22 100644 --- a/platform/ui-next/src/tailwind.css +++ b/platform/ui-next/src/tailwind.css @@ -4,6 +4,10 @@ @tailwind components; @tailwind utilities; +* { + min-height: 0 !important; +} + /* OHIF Theme */ @layer base { @@ -34,6 +38,21 @@ --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; --radius: 0.5rem; + --success-bg: hsl(143, 85%, 96%); + --success-border: hsl(145, 92%, 91%); + --success-text: hsl(140, 100%, 27%); + + --info-bg: hsl(208, 100%, 97%); + --info-border: hsl(221, 91%, 91%); + --info-text: hsl(210, 92%, 45%); + + --warning-bg: hsl(49, 100%, 97%); + --warning-border: hsl(49, 91%, 91%); + --warning-text: hsl(31, 92%, 45%); + + --error-bg: hsl(359, 100%, 97%); + --error-border: hsl(359, 100%, 94%); + --error-text: hsl(360, 100%, 45%); } .dark { diff --git a/platform/ui-next/src/types/ContextMenuItem.ts b/platform/ui-next/src/types/ContextMenuItem.ts new file mode 100644 index 000000000..5e856b058 --- /dev/null +++ b/platform/ui-next/src/types/ContextMenuItem.ts @@ -0,0 +1,9 @@ +export type ContextMenuItem = { + // A label to show for the item. + label: string; + // An icon to show the on right of the text - typically used for submenus + iconRight?: string; + // item is the menu item (eg the instance of this that is clicked on) + // props is the remaining properties passed to the context menu + action: (item, props) => void; +}; diff --git a/platform/ui-next/src/types/Predicate.ts b/platform/ui-next/src/types/Predicate.ts new file mode 100644 index 000000000..a14c760ea --- /dev/null +++ b/platform/ui-next/src/types/Predicate.ts @@ -0,0 +1 @@ +export type Predicate = (props: Record) => boolean; diff --git a/platform/ui-next/src/types/ThumbnailType.ts b/platform/ui-next/src/types/ThumbnailType.ts new file mode 100644 index 000000000..df7199dd7 --- /dev/null +++ b/platform/ui-next/src/types/ThumbnailType.ts @@ -0,0 +1,3 @@ +type ThumbnailType = 'thumbnail' | 'thumbnailTracked' | 'thumbnailNoImage'; + +export default ThumbnailType; diff --git a/platform/ui-next/src/types/ViewportActionCornersTypes.ts b/platform/ui-next/src/types/ViewportActionCornersTypes.ts new file mode 100644 index 000000000..f5f2f69ff --- /dev/null +++ b/platform/ui-next/src/types/ViewportActionCornersTypes.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export interface ViewportActionCornersComponentInfo { + id: string; + component: ReactNode; +} diff --git a/platform/ui-next/src/types/index.ts b/platform/ui-next/src/types/index.ts new file mode 100644 index 000000000..14053383d --- /dev/null +++ b/platform/ui-next/src/types/index.ts @@ -0,0 +1,20 @@ +import ThumbnailType from './ThumbnailType'; + +// A few miscellaneous types declared inline here. + +export * from './Predicate'; +export * from './ContextMenuItem'; +export * from './ViewportActionCornersTypes'; + +/** + * StringNumber often comes back from DICOMweb for integer valued items. + */ +type StringNumber = string | number; + +/** + * StringArray often comes back from dcmjs for single valued strings that + * might have multiple values such as window level descriptions. + */ +type StringArray = string | string[]; + +export type { StringNumber, StringArray, ThumbnailType }; diff --git a/platform/ui/src/components/ProgressDropdown/ProgressItemDetail.tsx b/platform/ui/src/components/ProgressDropdown/ProgressItemDetail.tsx index b0213690e..aaa2672d4 100644 --- a/platform/ui/src/components/ProgressDropdown/ProgressItemDetail.tsx +++ b/platform/ui/src/components/ProgressDropdown/ProgressItemDetail.tsx @@ -69,7 +69,9 @@ const ProgressItemDetail = ({ option }: { option: ProgressDropdownOption }): Rea )} {!info && iconContent} -
{label}
+
+ {label} +
); }; diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx index e96960a52..a2e133bc9 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx @@ -161,7 +161,7 @@ const SegmentationConfig = ({ /> )}
-
+
); }; diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx index 20d001408..b2923b04b 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx @@ -98,7 +98,7 @@ function SegmentationDropDownRow({ ], ]} > -
+
@@ -138,7 +138,7 @@ function SegmentationDropDownRow({ />
onToggleSegmentationVisibility(activeSegmentation.id)} > {activeSegmentation.isVisible ? ( diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx index e92c05bcf..f74367c10 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx @@ -94,7 +94,7 @@ const SegmentationGroupTable = ({ segmentationConfig={segmentationConfig} /> )} -
+
{segmentations?.length === 0 ? (
{showAddSegmentation && !disableEditing && ( diff --git a/platform/ui/src/components/StudyItem/StudyItem.tsx b/platform/ui/src/components/StudyItem/StudyItem.tsx index 6e99c48d1..03fbfddf9 100644 --- a/platform/ui/src/components/StudyItem/StudyItem.tsx +++ b/platform/ui/src/components/StudyItem/StudyItem.tsx @@ -49,7 +49,7 @@ const StudyItem = ({
{ +interface StudySummaryProps { + date: string; + modality: string; + description?: string; +} + +const StudySummary: React.FC = ({ date, modality, description }) => { return (
@@ -17,10 +22,4 @@ const StudySummary = ({ date, modality, description }) => { ); }; -StudySummary.propTypes = { - date: PropTypes.string.isRequired, - modality: PropTypes.string.isRequired, - description: PropTypes.string, -}; - export default StudySummary; diff --git a/platform/ui/src/components/ViewportPane/ViewportPane.tsx b/platform/ui/src/components/ViewportPane/ViewportPane.tsx index 33fd2174b..aa79c9a20 100644 --- a/platform/ui/src/components/ViewportPane/ViewportPane.tsx +++ b/platform/ui/src/components/ViewportPane/ViewportPane.tsx @@ -64,7 +64,7 @@ function ViewportPane({ onScroll={onInteractionHandler} onWheel={onInteractionHandler} className={classnames( - 'group h-full w-full overflow-hidden rounded-md transition duration-300', + 'group/pane h-full w-full overflow-hidden rounded-md transition duration-300', { 'border-primary-light border-2': isActive, 'border-2 border-transparent': !isActive, @@ -80,7 +80,7 @@ function ViewportPane({ 'h-full w-full overflow-hidden rounded-md', { 'border border-transparent': isActive, - 'border-secondary-light group-hover:border-primary-light/70 border': !isActive, + 'border-secondary-light group-hover/pane:border-primary-light/70 border': !isActive, }, className )} diff --git a/platform/ui/src/components/WindowLevel/WindowLevel.tsx b/platform/ui/src/components/WindowLevel/WindowLevel.tsx index 53a7258e5..9767ea326 100644 --- a/platform/ui/src/components/WindowLevel/WindowLevel.tsx +++ b/platform/ui/src/components/WindowLevel/WindowLevel.tsx @@ -143,11 +143,12 @@ const WindowLevel = ({ inputClassName="grow" maxValue={1} minValue={0} - step={0.1} + step={0.05} value={opacity} showLabel={true} allowNumberEdit={true} onChange={handleOpacityChange} + showAdjustmentArrows={false} />
)} diff --git a/platform/ui/src/contextProviders/CineProvider.tsx b/platform/ui/src/contextProviders/CineProvider.tsx index 97a568823..ad24c4fe3 100644 --- a/platform/ui/src/contextProviders/CineProvider.tsx +++ b/platform/ui/src/contextProviders/CineProvider.tsx @@ -93,6 +93,9 @@ export default function CineProvider({ children, service }) { setIsCineEnabled: isCineEnabled => service.setIsCineEnabled(isCineEnabled), playClip: (element, playClipOptions) => service.playClip(element, playClipOptions), stopClip: (element, stopClipOptions) => service.stopClip(element, stopClipOptions), + setViewportCineClosed: viewportId => service.setViewportCineClosed(viewportId), + clearViewportCineClosed: viewportId => service.clearViewportCineClosed(viewportId), + isViewportCineClosed: viewportId => service.isViewportCineClosed(viewportId), }; return {children}; diff --git a/platform/ui/src/contextProviders/DialogProvider.tsx b/platform/ui/src/contextProviders/DialogProvider.tsx index 9e0667ae7..647816220 100644 --- a/platform/ui/src/contextProviders/DialogProvider.tsx +++ b/platform/ui/src/contextProviders/DialogProvider.tsx @@ -1,4 +1,12 @@ -import React, { useState, createContext, useContext, useCallback, useEffect, useRef } from 'react'; +import React, { + useState, + createContext, + useContext, + useCallback, + useEffect, + useRef, + useMemo, +} from 'react'; import PropTypes from 'prop-types'; import Draggable from 'react-draggable'; @@ -268,8 +276,13 @@ const DialogProvider = ({ children, service = null }) => { const validCallback = callback => callback && typeof callback === 'function'; + const contextValue = useMemo( + () => ({ create, dismiss, dismissAll, isEmpty }), + [create, dismiss, dismissAll, isEmpty] + ); + return ( - + {!isEmpty() && (
; -} - -const DEFAULT_STATE: DefaultState = { +const DEFAULT_STATE: AppTypes.ViewportGrid.State = { activeViewportId: null, layout: { numRows: 0, @@ -78,7 +58,10 @@ const getViewportLabel = (viewports, viewportId) => { return viewportLabels[viewportIds.indexOf(viewportId)]; }; -const determineActiveViewportId = (state: DefaultState, newViewports: Map) => { +const determineActiveViewportId = ( + state: AppTypes.ViewportGrid.State, + newViewports: Map +) => { const { activeViewportId } = state; const currentActiveViewport = state.viewports.get(activeViewportId); @@ -132,10 +115,37 @@ const determineActiveViewportId = (state: DefaultState, newViewports: Map) => { return sortedViewports[0].viewportId; }; -export const ViewportGridContext = createContext(DEFAULT_STATE); +// Define the API interface +interface ViewportGridApi { + getState: () => AppTypes.ViewportGrid.State; + setActiveViewportId: (index: string) => void; + setDisplaySetsForViewport: (props: any) => void; + setDisplaySetsForViewports: (props: any[]) => void; + setLayout: (layout: AppTypes.ViewportGrid.Layout) => void; + reset: () => void; + set: (gridLayoutState: Partial) => void; + getNumViewportPanes: () => number; + setViewportIsReady: (viewportId: string, isReady: boolean) => void; + getGridViewportsReady: () => boolean; + getActiveViewportOptionByKey: (key: string) => any; + setViewportGridSizeChanged: (props: any) => void; + publishViewportsReady: () => void; +} -export function ViewportGridProvider({ children, service }) { - const viewportGridReducer = (state: DefaultState, action) => { +// Update the context type +export const ViewportGridContext = createContext<[AppTypes.ViewportGrid.State, ViewportGridApi]>([ + DEFAULT_STATE, + {} as ViewportGridApi, +]); + +// Update the provider props type +interface ViewportGridProviderProps { + children: ReactNode; + service: ViewportGridService; +} + +export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) { + const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => { switch (action.type) { case 'SET_ACTIVE_VIEWPORT_ID': { return { ...state, ...{ activeViewportId: action.payload } }; @@ -207,10 +217,10 @@ export function ViewportGridProvider({ children, service }) { // viewportLabel: getViewportLabel(viewports, viewportId), }; - viewportOptions.presentationIds = ViewportGridService.getPresentationIds( - newViewport, - viewports - ); + viewportOptions.presentationIds = service.getPresentationIds({ + viewport: newViewport, + viewports, + }); viewports.set(viewportId, { ...viewports.get(viewportId), @@ -233,7 +243,7 @@ export function ViewportGridProvider({ children, service }) { // If empty viewportOptions, we use numRow and numCols to calculate number of viewports const hasOptions = layoutOptions?.length; - const viewports = new Map(); + const viewports = new Map(); // Options is a temporary state store which can be used by the // findOrCreate to store state about already found viewports. Typically, // it will be used to store the display set UID's which are already @@ -298,10 +308,11 @@ export function ViewportGridProvider({ children, service }) { viewport.isReady = false; if (!viewport.viewportOptions.presentationIds) { - viewport.viewportOptions.presentationIds = ViewportGridService.getPresentationIds( + const presentationIds = service.getPresentationIds({ viewport, - viewports - ); + viewports, + }); + viewport.viewportOptions.presentationIds = presentationIds; } } } @@ -513,4 +524,6 @@ ViewportGridProvider.propTypes = { service: PropTypes.instanceOf(ViewportGridService).isRequired, }; -export const useViewportGrid = () => useContext(ViewportGridContext); +// Update the useViewportGrid hook +export const useViewportGrid = (): [AppTypes.ViewportGrid.State, ViewportGridApi] => + useContext(ViewportGridContext); diff --git a/platform/ui/src/tailwind.css b/platform/ui/src/tailwind.css index 32d2a9670..1c23d4c61 100644 --- a/platform/ui/src/tailwind.css +++ b/platform/ui/src/tailwind.css @@ -5,6 +5,10 @@ @tailwind components; @tailwind utilities; +* { + min-height: 0 !important; +} + @layer base { html { font-family: 'Inter', sans-serif; @@ -28,3 +32,76 @@ user-select: none; opacity: 0.5; } + +@layer base { + :root { + --highlight: 191 74% 63%; + --background: 236 62% 5%; + --foreground: 0 0% 98%; + --card: 236 62% 5%; + --card-foreground: 0 0% 98%; + --popover: 219 90% 15%; + --popover-foreground: 0 0% 98%; + --primary: 214 98% 60%; + --primary-foreground: 0 0% 98%; + --secondary: 214 66% 48%; + --secondary-foreground: 200 50% 84%; + --muted: 234 64% 10%; + --muted-foreground: 200 46% 65%; + --accent: 217 79% 24%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 236 45% 21%; + --ring: 214 98% 60%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --radius: 0.5rem; + --success-bg: hsl(143, 85%, 96%); + --success-border: hsl(145, 92%, 91%); + --success-text: hsl(140, 100%, 27%); + + --info-bg: hsl(208, 100%, 97%); + --info-border: hsl(221, 91%, 91%); + --info-text: hsl(210, 92%, 45%); + + --warning-bg: hsl(49, 100%, 97%); + --warning-border: hsl(49, 91%, 91%); + --warning-text: hsl(31, 92%, 45%); + + --error-bg: hsl(359, 100%, 97%); + --error-border: hsl(359, 100%, 94%); + --error-text: hsl(360, 100%, 45%); + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 214 98% 60%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 236 45% 21%; + --ring: 214 98% 60%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index ad6fb046e..585983bf1 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -253,6 +253,18 @@ module.exports = { fill: { current: 'currentColor', }, + fontSize: { + 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 + }, flex: { 1: '1 1 0%', 0.3: '0.3 0.3 0%', diff --git a/testdata b/testdata deleted file mode 160000 index bd743ed76..000000000 --- a/testdata +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bd743ed76b403e6d476e1af0bbfe7479d96b563d diff --git a/tests/SRHydration.spec.ts b/tests/SRHydration.spec.ts index 844f1908d..c47c696a2 100644 --- a/tests/SRHydration.spec.ts +++ b/tests/SRHydration.spec.ts @@ -14,6 +14,6 @@ test('should hydrate SR reports correctly', async ({ page }) => { await checkForScreenshot(page, page, screenShotPaths.srHydration.srPreHydration); await page.getByTestId('yes-hydrate-btn').click(); await checkForScreenshot(page, page, screenShotPaths.srHydration.srPostHydration); - await page.getByTestId('measurement-item').first().click(); + await page.getByTestId('data-row').first().click(); await checkForScreenshot(page, page, screenShotPaths.srHydration.srJumpToMeasurement); }); diff --git a/tsconfig.json b/tsconfig.json index ee01dec9e..32ef74117 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "baseUrl": "./", "checkJs": true, - // Ensure that Babel can safely transpile files in the TypeScript project "isolatedModules": true, "emitDeclarationOnly": true, "jsx": "react", @@ -10,14 +9,23 @@ "esModuleInterop": true, "sourceMap": true, "declaration": true, - "moduleResolution": "node", + "moduleResolution": "bundler", + "module": "esnext", + "target": "ES2022", "outDir": "./dist", "paths": { "@ohif/core": ["platform/core/src"], "@ohif/ui": ["platform/ui/src"], "@ohif/ui-next": ["platform/ui-next/src"], "@ohif/i18n": ["platform/i18n/src"], - "@ohif/app": ["platform/app/src"] + "@ohif/app": ["platform/app/src"], + "@ohif/extension-default": ["./extensions/default/src"], + "@ohif/extension-measurement-tracking": ["./extensions/measurement-tracking/src"], + "@ohif/extension-cornerstone-dicom-sr": ["./extensions/cornerstone-dicom-sr/src"], + "@ohif/extension-cornerstone-dicom-seg": ["./extensions/cornerstone-dicom-seg/src"], + "@ohif/extension-cornerstone": ["./extensions/cornerstone/src"], + "@ohif/extension-dicom-pdf": ["./extensions/dicom-pdf/src"], + "@state": ["./platform/app/src/state"] } }, "include": [ @@ -25,7 +33,7 @@ "platform/**/public/**/*", "extensions/**/src/**/*", "modes/**/src/**/*", - "tests/**/*", + "tests/**/*" ], "exclude": ["node_modules", "dist"] } diff --git a/version.mjs b/version.mjs index fa63e4f7a..aa00aa267 100644 --- a/version.mjs +++ b/version.mjs @@ -22,7 +22,8 @@ async function run() { if (branchName.startsWith('release')) { console.log('Branch: release'); await fs.writeFile('./commit.txt', currentCommitHash); - console.log('the version is automatically picked up from the version.txt file'); + const version = await fs.readFile('./version.txt', 'utf-8'); + console.log('Version from version.txt:', version.trim()); return; } else { console.log('Branch: master'); diff --git a/yarn.lock b/yarn.lock index c144964f3..16fde4e30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -310,15 +310,7 @@ dependencies: default-browser-id "3.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.8.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== - dependencies: - "@babel/highlight" "^7.24.7" - picocolors "^1.0.0" - -"@babel/code-frame@^7.25.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.25.7", "@babel/code-frame@^7.8.3": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== @@ -326,16 +318,30 @@ "@babel/highlight" "^7.25.7" picocolors "^1.0.0" +"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.2", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== -"@babel/compat-data@^7.25.7", "@babel/compat-data@^7.25.8": +"@babel/compat-data@^7.25.7": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402" integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== +"@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" + integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== + "@babel/core@7.24.7", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.21.3", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" @@ -357,7 +363,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/core@^7.23.3", "@babel/core@^7.24.4": +"@babel/core@^7.24.4": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.8.tgz#a57137d2a51bbcffcfaeba43cb4dd33ae3e0e1c6" integrity sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg== @@ -378,17 +384,28 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.0", "@babel/generator@^7.24.7", "@babel/generator@^7.7.2": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" - integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== +"@babel/core@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== dependencies: - "@babel/types" "^7.24.7" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^2.5.1" + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" -"@babel/generator@^7.23.3", "@babel/generator@^7.25.7": +"@babel/generator@^7.23.0", "@babel/generator@^7.24.7", "@babel/generator@^7.25.7", "@babel/generator@^7.7.2": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56" integrity sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA== @@ -398,6 +415,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" +"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" + integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== + dependencies: + "@babel/parser" "^7.26.2" + "@babel/types" "^7.26.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -412,15 +440,14 @@ dependencies: "@babel/types" "^7.25.7" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" - integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/types" "^7.25.9" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.7": +"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz#d721650c1f595371e0a23ee816f1c3c488c0d622" integrity sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg== @@ -428,6 +455,14 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz#f41752fe772a578e67286e6779a68a5a92de1ee9" + integrity sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9" @@ -450,7 +485,18 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.24.7": +"@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== + dependencies: + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" integrity sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg== @@ -465,7 +511,7 @@ "@babel/helper-split-export-declaration" "^7.24.7" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.25.7": +"@babel/helper-create-class-features-plugin@^7.24.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz#5d65074c76cae75607421c00d6bd517fe1892d6b" integrity sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw== @@ -478,6 +524,19 @@ "@babel/traverse" "^7.25.7" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" @@ -487,12 +546,12 @@ regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" - integrity sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ== +"@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz#3e8999db94728ad2b2458d7a470e7770b7764e26" + integrity sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" regexpu-core "^6.1.1" semver "^6.3.1" @@ -518,7 +577,7 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": +"@babel/helper-define-polyfill-provider@^0.6.2": version "0.6.2" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== @@ -551,15 +610,7 @@ dependencies: "@babel/types" "^7.24.7" -"@babel/helper-member-expression-to-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz#67613d068615a70e4ed5101099affc7a41c5225f" - integrity sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-member-expression-to-functions@^7.25.7": +"@babel/helper-member-expression-to-functions@^7.24.7", "@babel/helper-member-expression-to-functions@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz#541a33b071f0355a63a0fa4bdf9ac360116b8574" integrity sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA== @@ -567,6 +618,14 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" @@ -583,6 +642,14 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-module-transforms@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" @@ -604,40 +671,40 @@ "@babel/helper-validator-identifier" "^7.25.7" "@babel/traverse" "^7.25.7" -"@babel/helper-optimise-call-expression@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" - integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== dependencies: - "@babel/types" "^7.24.7" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-optimise-call-expression@^7.25.7": +"@babel/helper-optimise-call-expression@^7.24.7", "@babel/helper-optimise-call-expression@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz#1de1b99688e987af723eed44fa7fc0ee7b97d77a" integrity sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng== dependencies: "@babel/types" "^7.25.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" - integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== + dependencies: + "@babel/types" "^7.25.9" -"@babel/helper-plugin-utils@^7.25.7": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== -"@babel/helper-remap-async-to-generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" - integrity sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-wrap-function" "^7.24.7" +"@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== -"@babel/helper-remap-async-to-generator@^7.25.7": +"@babel/helper-remap-async-to-generator@^7.24.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz#9efdc39df5f489bcd15533c912b6c723a0a65021" integrity sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw== @@ -646,16 +713,16 @@ "@babel/helper-wrap-function" "^7.25.7" "@babel/traverse" "^7.25.7" -"@babel/helper-replace-supers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" - integrity sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg== +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== dependencies: - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.7" - "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-replace-supers@^7.25.7": +"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz#38cfda3b6e990879c71d08d0fef9236b62bd75f5" integrity sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw== @@ -664,6 +731,15 @@ "@babel/helper-optimise-call-expression" "^7.25.7" "@babel/traverse" "^7.25.7" +"@babel/helper-replace-supers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz#ba447224798c3da3f8713fc272b145e33da6a5c5" + integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/helper-simple-access@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" @@ -680,6 +756,14 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-simple-access@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz#6d51783299884a2c74618d6ef0f86820ec2e7739" + integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" @@ -696,6 +780,14 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-split-export-declaration@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" @@ -703,16 +795,16 @@ dependencies: "@babel/types" "^7.24.7" -"@babel/helper-string-parser@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" - integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== - -"@babel/helper-string-parser@^7.25.7": +"@babel/helper-string-parser@^7.24.7", "@babel/helper-string-parser@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" @@ -723,6 +815,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" @@ -733,15 +830,10 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== -"@babel/helper-wrap-function@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" - integrity sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw== - dependencies: - "@babel/helper-function-name" "^7.24.7" - "@babel/template" "^7.24.7" - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== "@babel/helper-wrap-function@^7.25.7": version "7.25.7" @@ -752,6 +844,15 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helpers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416" @@ -768,15 +869,13 @@ "@babel/template" "^7.25.7" "@babel/types" "^7.25.7" -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" "@babel/highlight@^7.25.7": version "7.25.7" @@ -800,6 +899,13 @@ dependencies: "@babel/types" "^7.25.8" +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" + integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== + dependencies: + "@babel/types" "^7.26.0" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" @@ -808,20 +914,20 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz#93969ac50ef4d68b2504b01b758af714e4cbdd64" - integrity sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz#a338d611adb9dcd599b8b1efa200c88ebeffe046" - integrity sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": version "7.24.7" @@ -830,12 +936,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz#c5f755e911dfac7ef6957300c0f9c4a8c18c06f4" - integrity sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": version "7.24.7" @@ -846,14 +952,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-transform-optional-chaining" "^7.24.7" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz#3b7ea04492ded990978b6deaa1dfca120ad4455a" - integrity sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/plugin-transform-optional-chaining" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": version "7.24.7" @@ -863,13 +969,13 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz#9622b1d597a703aa3a921e6f58c9c2d9a028d2c5" - integrity sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" "@babel/plugin-proposal-class-properties@^7.16.7": version "7.18.6" @@ -927,7 +1033,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": +"@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== @@ -955,27 +1061,27 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz#d1759e84dd4b437cf9fae69b4c06c41d7625bfb7" - integrity sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw== +"@babel/plugin-syntax-flow@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.25.7.tgz#7d1255201b55d7644c57e0eb354aaf9f8b8d2d02" + integrity sha512-fyoj6/YdVtlv2ROig/J0fP7hh/wNO1MJGm1NR70Pg7jbkF+jOUL9joorqaCOQh06Y+LfgTagHzC8KqZ3MF782w== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-syntax-import-assertions@^7.22.5", "@babel/plugin-syntax-import-assertions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" - integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-import-assertions@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz#8ce248f9f4ed4b7ed4cb2e0eb4ed9efd9f52921f" integrity sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ== dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-import-attributes@^7.22.5", "@babel/plugin-syntax-import-attributes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" @@ -983,14 +1089,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-syntax-import-attributes@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz#d78dd0499d30df19a598e63ab895e21b909bc43f" - integrity sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw== +"@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -1018,7 +1124,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": +"@babel/plugin-syntax-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== @@ -1032,7 +1145,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": +"@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -1067,7 +1180,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": +"@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== @@ -1075,19 +1188,19 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.24.7", "@babel/plugin-syntax-typescript@^7.7.2": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" - integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-typescript@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz#bfc05b0cc31ebd8af09964650cee723bb228108b" integrity sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g== dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-syntax-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -1097,19 +1210,19 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-arrow-functions@^7.16.7", "@babel/plugin-transform-arrow-functions@^7.22.5", "@babel/plugin-transform-arrow-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" - integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-arrow-functions@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz#1b9ed22e6890a0e9ff470371c73b8c749bcec386" integrity sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg== dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-async-generator-functions@^7.23.2", "@babel/plugin-transform-async-generator-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" @@ -1120,14 +1233,14 @@ "@babel/helper-remap-async-to-generator" "^7.24.7" "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-transform-async-generator-functions@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz#3331de02f52cc1f2c75b396bec52188c85b0b1ec" - integrity sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA== +"@babel/plugin-transform-async-generator-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz#1b18530b077d18a407c494eb3d1d72da505283a2" + integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-remap-async-to-generator" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.25.9" "@babel/plugin-transform-async-to-generator@^7.22.5", "@babel/plugin-transform-async-to-generator@^7.24.7": version "7.24.7" @@ -1138,14 +1251,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-remap-async-to-generator" "^7.24.7" -"@babel/plugin-transform-async-to-generator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz#a44c7323f8d4285a6c568dd43c5c361d6367ec52" - integrity sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg== +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== dependencies: - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-remap-async-to-generator" "^7.25.7" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" "@babel/plugin-transform-block-scoped-functions@^7.22.5", "@babel/plugin-transform-block-scoped-functions@^7.24.7": version "7.24.7" @@ -1154,12 +1267,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-block-scoped-functions@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz#e0b8843d5571719a2f1bf7e284117a3379fcc17c" - integrity sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ== +"@babel/plugin-transform-block-scoped-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz#5700691dbd7abb93de300ca7be94203764fce458" + integrity sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-block-scoping@^7.23.0", "@babel/plugin-transform-block-scoping@^7.24.7": version "7.24.7" @@ -1168,12 +1281,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-block-scoping@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz#6dab95e98adf780ceef1b1c3ab0e55cd20dd410a" - integrity sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow== +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz#c33665e46b06759c93687ca0f84395b80c0473a1" + integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-class-properties@^7.22.5", "@babel/plugin-transform-class-properties@^7.24.7": version "7.24.7" @@ -1183,13 +1296,13 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-class-properties@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz#a389cfca7a10ac80e3ff4c75fca08bd097ad1523" - integrity sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g== +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-class-static-block@^7.22.11", "@babel/plugin-transform-class-static-block@^7.24.7": version "7.24.7" @@ -1200,13 +1313,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-class-static-block@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz#a8af22028920fe404668031eceb4c3aadccb5262" - integrity sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ== +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-classes@^7.22.15", "@babel/plugin-transform-classes@^7.24.7": version "7.24.7" @@ -1222,16 +1335,16 @@ "@babel/helper-split-export-declaration" "^7.24.7" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz#5103206cf80d02283bbbd044509ea3b65d0906bb" - integrity sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg== +"@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.22.5", "@babel/plugin-transform-computed-properties@^7.24.7": @@ -1242,13 +1355,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/template" "^7.24.7" -"@babel/plugin-transform-computed-properties@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz#7f621f0aa1354b5348a935ab12e3903842466f65" - integrity sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA== +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/template" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" "@babel/plugin-transform-destructuring@^7.23.0", "@babel/plugin-transform-destructuring@^7.24.7": version "7.24.7" @@ -1257,12 +1370,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-destructuring@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz#f6f26a9feefb5aa41fd45b6f5838901b5333d560" - integrity sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA== +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-dotall-regex@^7.22.5", "@babel/plugin-transform-dotall-regex@^7.24.7": version "7.24.7" @@ -1272,13 +1385,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-dotall-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz#9d775c4a3ff1aea64045300fcd4309b4a610ef02" - integrity sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ== +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-duplicate-keys@^7.22.5", "@babel/plugin-transform-duplicate-keys@^7.24.7": version "7.24.7" @@ -1287,35 +1400,35 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-duplicate-keys@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz#fbba7d1155eab76bd4f2a038cbd5d65883bd7a93" - integrity sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg== +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz#102b31608dcc22c08fbca1894e104686029dc141" - integrity sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-dynamic-import@^7.22.11", "@babel/plugin-transform-dynamic-import@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" - integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-transform-dynamic-import@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz#f1edbe75b248cf44c70c8ca8ed3818a668753aaa" - integrity sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.7.tgz#31905ab2cfa94dcf1b1f8ce66096720b2908e518" + integrity sha512-UvcLuual4h7/GfylKm2IAA3aph9rwvAM2XBA0uPKU3lca+Maai4jBjjEVUS568ld6kJcgbouuumCBhMd/Yz17w== dependencies: "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-exponentiation-operator@^7.22.5", "@babel/plugin-transform-exponentiation-operator@^7.24.7": version "7.24.7" @@ -1325,13 +1438,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-exponentiation-operator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz#5961a3a23a398faccd6cddb34a2182807d75fb5f" - integrity sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg== +"@babel/plugin-transform-exponentiation-operator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz#ece47b70d236c1d99c263a1e22b62dc20a4c8b0f" + integrity sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-export-namespace-from@^7.22.11", "@babel/plugin-transform-export-namespace-from@^7.24.7": version "7.24.7" @@ -1341,30 +1454,22 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-export-namespace-from@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz#d1988c3019a380b417e0516418b02804d3858145" - integrity sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw== +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-flow-strip-types@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.7.tgz#32be871a80e10bbe6d8b1c8a7eeedbbc896d5e80" + integrity sha512-q8Td2PPc6/6I73g96SreSUCKEcwMXCwcXSIAVTyTTN6CpJe0dMj8coxu1fg1T9vfBLi6Rsi6a4ECcFBbKabS5w== dependencies: "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-flow-strip-types@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz#ae454e62219288fbb734541ab00389bfb13c063e" - integrity sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-flow" "^7.24.7" + "@babel/plugin-syntax-flow" "^7.25.7" "@babel/plugin-transform-for-of@^7.22.15", "@babel/plugin-transform-for-of@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" - integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - -"@babel/plugin-transform-for-of@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz#0acfea0f27aa290818b5b48a5a44b3f03fc13669" integrity sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw== @@ -1372,6 +1477,14 @@ "@babel/helper-plugin-utils" "^7.25.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" +"@babel/plugin-transform-for-of@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz#4bdc7d42a213397905d89f02350c5267866d5755" + integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-function-name@^7.22.5", "@babel/plugin-transform-function-name@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" @@ -1381,14 +1494,14 @@ "@babel/helper-function-name" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-function-name@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz#7e394ccea3693902a8b50ded8b6ae1fa7b8519fd" - integrity sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ== +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== dependencies: - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" "@babel/plugin-transform-json-strings@^7.22.11", "@babel/plugin-transform-json-strings@^7.24.7": version "7.24.7" @@ -1398,12 +1511,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-json-strings@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz#6fb3ec383a2ea92652289fdba653e3f9de722694" - integrity sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA== +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-literals@^7.22.5", "@babel/plugin-transform-literals@^7.24.7": version "7.24.7" @@ -1412,12 +1525,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz#70cbdc742f2cfdb1a63ea2cbd018d12a60b213c3" - integrity sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w== +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-logical-assignment-operators@^7.22.11", "@babel/plugin-transform-logical-assignment-operators@^7.24.7": version "7.24.7" @@ -1427,12 +1540,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-transform-logical-assignment-operators@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz#01868ff92daa9e525b4c7902aa51979082a05710" - integrity sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g== +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-member-expression-literals@^7.22.5", "@babel/plugin-transform-member-expression-literals@^7.24.7": version "7.24.7" @@ -1441,12 +1554,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-member-expression-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz#0a36c3fbd450cc9e6485c507f005fa3d1bc8fca5" - integrity sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw== +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-modules-amd@^7.23.0", "@babel/plugin-transform-modules-amd@^7.24.7": version "7.24.7" @@ -1456,13 +1569,13 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-modules-amd@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz#bb4e543b5611f6c8c685a2fd485408713a3adf3d" - integrity sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA== +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-modules-commonjs@^7.23.0", "@babel/plugin-transform-modules-commonjs@^7.24.7": version "7.24.7" @@ -1473,14 +1586,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-simple-access" "^7.24.7" -"@babel/plugin-transform-modules-commonjs@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz#173f0c791bb7407c092ce6d77ee90eb3f2d1d2fd" - integrity sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg== +"@babel/plugin-transform-modules-commonjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz#d165c8c569a080baf5467bda88df6425fc060686" + integrity sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-simple-access" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-simple-access" "^7.25.9" "@babel/plugin-transform-modules-systemjs@^7.23.0", "@babel/plugin-transform-modules-systemjs@^7.24.7": version "7.24.7" @@ -1492,15 +1605,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" -"@babel/plugin-transform-modules-systemjs@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz#8b14d319a177cc9c85ef8b0512afd429d9e2e60b" - integrity sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g== +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-identifier" "^7.25.7" - "@babel/traverse" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" "@babel/plugin-transform-modules-umd@^7.22.5", "@babel/plugin-transform-modules-umd@^7.24.7": version "7.24.7" @@ -1510,13 +1623,13 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-modules-umd@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz#00ee7a7e124289549381bfb0e24d87fd7f848367" - integrity sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw== +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-named-capturing-groups-regex@^7.22.5", "@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": version "7.24.7" @@ -1526,13 +1639,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-named-capturing-groups-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz#a2f3f6d7f38693b462542951748f0a72a34d196d" - integrity sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow== +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-new-target@^7.22.5", "@babel/plugin-transform-new-target@^7.24.7": version "7.24.7" @@ -1541,12 +1654,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-new-target@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz#52b2bde523b76c548749f38dc3054f1f45e82bc9" - integrity sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA== +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-nullish-coalescing-operator@^7.22.11", "@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": version "7.24.7" @@ -1556,12 +1669,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-nullish-coalescing-operator@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz#befb4900c130bd52fccf2b926314557987f1b552" - integrity sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ== +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz#bcb1b0d9e948168102d5f7104375ca21c3266949" + integrity sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-numeric-separator@^7.22.11", "@babel/plugin-transform-numeric-separator@^7.24.7": version "7.24.7" @@ -1571,12 +1684,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-numeric-separator@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz#91e370486371637bd42161052f2602c701386891" - integrity sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q== +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-object-rest-spread@^7.22.15", "@babel/plugin-transform-object-rest-spread@^7.24.7": version "7.24.7" @@ -1588,14 +1701,14 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.24.7" -"@babel/plugin-transform-object-rest-spread@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz#0904ac16bcce41df4db12d915d6780f85c7fb04b" - integrity sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g== +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== dependencies: - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-transform-parameters" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" "@babel/plugin-transform-object-super@^7.22.5", "@babel/plugin-transform-object-super@^7.24.7": version "7.24.7" @@ -1605,13 +1718,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-replace-supers" "^7.24.7" -"@babel/plugin-transform-object-super@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz#582a9cea8cf0a1e02732be5b5a703a38dedf5661" - integrity sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA== +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" "@babel/plugin-transform-optional-catch-binding@^7.22.11", "@babel/plugin-transform-optional-catch-binding@^7.24.7": version "7.24.7" @@ -1621,12 +1734,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-catch-binding@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz#2649b86a3bb202c6894ec81a6ddf41b94d8f3103" - integrity sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg== +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-optional-chaining@^7.23.0", "@babel/plugin-transform-optional-chaining@^7.24.7": version "7.24.7" @@ -1637,13 +1750,13 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.25.7", "@babel/plugin-transform-optional-chaining@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz#f46283b78adcc5b6ab988a952f989e7dce70653f" - integrity sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg== +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.22.15", "@babel/plugin-transform-parameters@^7.24.7": version "7.24.7" @@ -1652,12 +1765,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-parameters@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz#80c38b03ef580f6d6bffe1c5254bb35986859ac7" - integrity sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ== +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-private-methods@^7.22.5", "@babel/plugin-transform-private-methods@^7.24.7": version "7.24.7" @@ -1667,13 +1780,13 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-private-methods@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz#c790a04f837b4bd61d6b0317b43aa11ff67dce80" - integrity sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA== +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-private-property-in-object@^7.22.11", "@babel/plugin-transform-private-property-in-object@^7.24.7": version "7.24.7" @@ -1685,14 +1798,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-transform-private-property-in-object@^7.25.8": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz#1234f856ce85e061f9688764194e51ea7577c434" - integrity sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow== +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-property-literals@^7.22.5", "@babel/plugin-transform-property-literals@^7.24.7": version "7.24.7" @@ -1701,12 +1814,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-property-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz#a8612b4ea4e10430f00012ecf0155662c7d6550d" - integrity sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw== +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-react-constant-elements@^7.12.1", "@babel/plugin-transform-react-constant-elements@^7.21.3": version "7.24.7" @@ -1715,13 +1828,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-react-display-name@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz#9caff79836803bc666bcfe210aeb6626230c293b" - integrity sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-transform-react-display-name@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.7.tgz#2753e875a1b702fb1d806c4f5d4c194d64cadd88" @@ -1729,12 +1835,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-react-jsx-development@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" - integrity sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ== +"@babel/plugin-transform-react-display-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz#4b79746b59efa1f38c8695065a92a9f5afb24f7d" + integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== dependencies: - "@babel/plugin-transform-react-jsx" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-react-jsx-development@^7.25.7": version "7.25.7" @@ -1743,16 +1849,12 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.25.7" -"@babel/plugin-transform-react-jsx@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" - integrity sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA== +"@babel/plugin-transform-react-jsx-development@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz#8fd220a77dd139c07e25225a903b8be8c829e0d7" + integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/plugin-transform-react-jsx" "^7.25.9" "@babel/plugin-transform-react-jsx@^7.25.7": version "7.25.7" @@ -1765,13 +1867,16 @@ "@babel/plugin-syntax-jsx" "^7.25.7" "@babel/types" "^7.25.7" -"@babel/plugin-transform-react-pure-annotations@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" - integrity sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA== +"@babel/plugin-transform-react-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz#06367940d8325b36edff5e2b9cbe782947ca4166" + integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/types" "^7.25.9" "@babel/plugin-transform-react-pure-annotations@^7.25.7": version "7.25.7" @@ -1781,6 +1886,14 @@ "@babel/helper-annotate-as-pure" "^7.25.7" "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-react-pure-annotations@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz#ea1c11b2f9dbb8e2d97025f43a3b5bc47e18ae62" + integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-regenerator@^7.16.7", "@babel/plugin-transform-regenerator@^7.22.10", "@babel/plugin-transform-regenerator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" @@ -1789,14 +1902,22 @@ "@babel/helper-plugin-utils" "^7.24.7" regenerator-transform "^0.15.2" -"@babel/plugin-transform-regenerator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz#6eb006e6d26f627bc2f7844a9f19770721ad6f3e" - integrity sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ== +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz#03a8a4670d6cebae95305ac6defac81ece77740b" + integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" regenerator-transform "^0.15.2" +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-reserved-words@^7.22.5", "@babel/plugin-transform-reserved-words@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" @@ -1804,12 +1925,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-reserved-words@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz#dc56b25e02afaabef3ce0c5b06b0916e8523e995" - integrity sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng== +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-runtime@7.24.7": version "7.24.7" @@ -1823,13 +1944,13 @@ babel-plugin-polyfill-regenerator "^0.6.1" semver "^6.3.1" -"@babel/plugin-transform-runtime@^7.22.9": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.7.tgz#435a4fab67273f00047dc806e05069c9c6344e12" - integrity sha512-Y9p487tyTzB0yDYQOtWnC+9HGOuogtP3/wNpun1xJXEEvI6vip59BSBTsHnekZLqxmPcgsrAKt46HAAb//xGhg== +"@babel/plugin-transform-runtime@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz#62723ea3f5b31ffbe676da9d6dae17138ae580ea" + integrity sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ== dependencies: - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" babel-plugin-polyfill-corejs2 "^0.4.10" babel-plugin-polyfill-corejs3 "^0.10.6" babel-plugin-polyfill-regenerator "^0.6.1" @@ -1842,12 +1963,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-shorthand-properties@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz#92690a9c671915602d91533c278cc8f6bf12275f" - integrity sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA== +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-spread@^7.22.5", "@babel/plugin-transform-spread@^7.24.7": version "7.24.7" @@ -1857,13 +1978,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" -"@babel/plugin-transform-spread@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz#df83e899a9fc66284ee601a7b738568435b92998" - integrity sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw== +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" "@babel/plugin-transform-sticky-regex@^7.22.5", "@babel/plugin-transform-sticky-regex@^7.24.7": version "7.24.7" @@ -1872,12 +1993,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-sticky-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz#341c7002bef7f29037be7fb9684e374442dd0d17" - integrity sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw== +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-template-literals@^7.22.5", "@babel/plugin-transform-template-literals@^7.24.7": version "7.24.7" @@ -1886,12 +2007,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-template-literals@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz#e566c581bb16d8541dd8701093bb3457adfce16b" - integrity sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA== +"@babel/plugin-transform-template-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz#6dbd4a24e8fad024df76d1fac6a03cf413f60fe1" + integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-typeof-symbol@^7.22.5", "@babel/plugin-transform-typeof-symbol@^7.24.7": version "7.24.7" @@ -1900,12 +2021,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-typeof-symbol@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz#debb1287182efd20488f126be343328c679b66eb" - integrity sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA== +"@babel/plugin-transform-typeof-symbol@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz#224ba48a92869ddbf81f9b4a5f1204bbf5a2bc4b" + integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-typescript@^7.13.0", "@babel/plugin-transform-typescript@^7.24.7": version "7.24.7" @@ -1917,16 +2038,16 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-typescript" "^7.24.7" -"@babel/plugin-transform-typescript@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz#8fc7c3d28ddd36bce45b9b48594129d0e560cfbe" - integrity sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q== +"@babel/plugin-transform-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz#69267905c2b33c2ac6d8fe765e9dc2ddc9df3849" + integrity sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/plugin-syntax-typescript" "^7.25.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-syntax-typescript" "^7.25.9" "@babel/plugin-transform-unicode-escapes@^7.22.10", "@babel/plugin-transform-unicode-escapes@^7.24.7": version "7.24.7" @@ -1935,12 +2056,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-escapes@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz#973592b6d13a914794e1de8cf1383e50e0f87f81" - integrity sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ== +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-unicode-property-regex@^7.22.5", "@babel/plugin-transform-unicode-property-regex@^7.24.7": version "7.24.7" @@ -1950,13 +2071,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-property-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz#25349197cce964b1343f74fa7cfdf791a1b1919e" - integrity sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w== +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-unicode-regex@^7.22.5", "@babel/plugin-transform-unicode-regex@^7.24.7": version "7.24.7" @@ -1966,13 +2087,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz#f93a93441baf61f713b6d5552aaa856bfab34809" - integrity sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g== +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-unicode-sets-regex@^7.22.5", "@babel/plugin-transform-unicode-sets-regex@^7.24.7": version "7.24.7" @@ -1982,13 +2103,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-unicode-sets-regex@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz#d1b3295d29e0f8f4df76abc909ad1ebee919560c" - integrity sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw== +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/preset-env@7.23.2": version "7.23.2" @@ -2163,73 +2284,74 @@ core-js-compat "^3.31.0" semver "^6.3.1" -"@babel/preset-env@^7.22.9", "@babel/preset-env@^7.23.3": - version "7.25.8" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.8.tgz#dc6b719627fb29cd9cccbbbe041802fd575b524c" - integrity sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg== +"@babel/preset-env@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.0.tgz#30e5c6bc1bcc54865bff0c5a30f6d4ccdc7fa8b1" + integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== dependencies: - "@babel/compat-data" "^7.25.8" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-option" "^7.25.7" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.7" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.7" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.7" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.7" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.7" + "@babel/compat-data" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.25.7" - "@babel/plugin-syntax-import-attributes" "^7.25.7" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.25.7" - "@babel/plugin-transform-async-generator-functions" "^7.25.8" - "@babel/plugin-transform-async-to-generator" "^7.25.7" - "@babel/plugin-transform-block-scoped-functions" "^7.25.7" - "@babel/plugin-transform-block-scoping" "^7.25.7" - "@babel/plugin-transform-class-properties" "^7.25.7" - "@babel/plugin-transform-class-static-block" "^7.25.8" - "@babel/plugin-transform-classes" "^7.25.7" - "@babel/plugin-transform-computed-properties" "^7.25.7" - "@babel/plugin-transform-destructuring" "^7.25.7" - "@babel/plugin-transform-dotall-regex" "^7.25.7" - "@babel/plugin-transform-duplicate-keys" "^7.25.7" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.7" - "@babel/plugin-transform-dynamic-import" "^7.25.8" - "@babel/plugin-transform-exponentiation-operator" "^7.25.7" - "@babel/plugin-transform-export-namespace-from" "^7.25.8" - "@babel/plugin-transform-for-of" "^7.25.7" - "@babel/plugin-transform-function-name" "^7.25.7" - "@babel/plugin-transform-json-strings" "^7.25.8" - "@babel/plugin-transform-literals" "^7.25.7" - "@babel/plugin-transform-logical-assignment-operators" "^7.25.8" - "@babel/plugin-transform-member-expression-literals" "^7.25.7" - "@babel/plugin-transform-modules-amd" "^7.25.7" - "@babel/plugin-transform-modules-commonjs" "^7.25.7" - "@babel/plugin-transform-modules-systemjs" "^7.25.7" - "@babel/plugin-transform-modules-umd" "^7.25.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.7" - "@babel/plugin-transform-new-target" "^7.25.7" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.8" - "@babel/plugin-transform-numeric-separator" "^7.25.8" - "@babel/plugin-transform-object-rest-spread" "^7.25.8" - "@babel/plugin-transform-object-super" "^7.25.7" - "@babel/plugin-transform-optional-catch-binding" "^7.25.8" - "@babel/plugin-transform-optional-chaining" "^7.25.8" - "@babel/plugin-transform-parameters" "^7.25.7" - "@babel/plugin-transform-private-methods" "^7.25.7" - "@babel/plugin-transform-private-property-in-object" "^7.25.8" - "@babel/plugin-transform-property-literals" "^7.25.7" - "@babel/plugin-transform-regenerator" "^7.25.7" - "@babel/plugin-transform-reserved-words" "^7.25.7" - "@babel/plugin-transform-shorthand-properties" "^7.25.7" - "@babel/plugin-transform-spread" "^7.25.7" - "@babel/plugin-transform-sticky-regex" "^7.25.7" - "@babel/plugin-transform-template-literals" "^7.25.7" - "@babel/plugin-transform-typeof-symbol" "^7.25.7" - "@babel/plugin-transform-unicode-escapes" "^7.25.7" - "@babel/plugin-transform-unicode-property-regex" "^7.25.7" - "@babel/plugin-transform-unicode-regex" "^7.25.7" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.7" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.25.9" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.25.9" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.25.9" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.25.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.25.9" + "@babel/plugin-transform-typeof-symbol" "^7.25.9" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" "@babel/preset-modules" "0.1.6-no-external-plugins" babel-plugin-polyfill-corejs2 "^0.4.10" babel-plugin-polyfill-corejs3 "^0.10.6" @@ -2238,13 +2360,13 @@ semver "^6.3.1" "@babel/preset-flow@^7.22.15": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.24.7.tgz#eef5cb8e05e97a448fc50c16826f5612fe512c06" - integrity sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.25.7.tgz#a9460677c182c2e105c32567a036d360c86668a9" + integrity sha512-q2x3g0YHzo/Ohsr51KOYS/BtZMsvkzVd8qEyhZAyTatYdobfgXCuyppTqTuIhdq5kR/P3nyyVvZ6H5dMc4PnCQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-transform-flow-strip-types" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-transform-flow-strip-types" "^7.25.7" "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" @@ -2256,18 +2378,6 @@ esutils "^2.0.2" "@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.7", "@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.15": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" - integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-transform-react-display-name" "^7.24.7" - "@babel/plugin-transform-react-jsx" "^7.24.7" - "@babel/plugin-transform-react-jsx-development" "^7.24.7" - "@babel/plugin-transform-react-pure-annotations" "^7.24.7" - -"@babel/preset-react@^7.22.5": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.7.tgz#081cbe1dea363b732764d06a0fdda67ffa17735d" integrity sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg== @@ -2279,6 +2389,18 @@ "@babel/plugin-transform-react-jsx-development" "^7.25.7" "@babel/plugin-transform-react-pure-annotations" "^7.25.7" +"@babel/preset-react@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.9.tgz#5f473035dc2094bcfdbc7392d0766bd42dce173e" + integrity sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-transform-react-display-name" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx-development" "^7.25.9" + "@babel/plugin-transform-react-pure-annotations" "^7.25.9" + "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.23.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" @@ -2290,21 +2412,21 @@ "@babel/plugin-transform-modules-commonjs" "^7.24.7" "@babel/plugin-transform-typescript" "^7.24.7" -"@babel/preset-typescript@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz#43c5b68eccb856ae5b52274b77b1c3c413cde1b7" - integrity sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw== +"@babel/preset-typescript@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz#4a570f1b8d104a242d923957ffa1eaff142a106d" + integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-option" "^7.25.7" - "@babel/plugin-syntax-jsx" "^7.25.7" - "@babel/plugin-transform-modules-commonjs" "^7.25.7" - "@babel/plugin-transform-typescript" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-typescript" "^7.25.9" "@babel/register@^7.22.15": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.24.6.tgz#59e21dcc79e1d04eed5377633b0f88029a6bef9e" - integrity sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.25.7.tgz#75ec0d3a8f843d344c51bf2f18fcc03f3a4c9117" + integrity sha512-qHTd2Rhn/rKhSUwdY6+n98FmwXN+N+zxSVx3zWqRe9INyvTpv+aQ5gDV2+43ACd3VtMBzPPljbb0gZb8u5ma6Q== dependencies: clone-deep "^4.0.1" find-cache-dir "^2.0.0" @@ -2318,9 +2440,9 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs2@^7.17.8": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.24.7.tgz#28e73d50c44812060a58548c5096be4243ee3ee5" - integrity sha512-+Lf6xofiPZLtFwNkpjGHPgJck4b22Yo8h9+WHf3bEbS4ikOyOMNtJk6HSTolEQ2irH1XSoeguaCkrkcgyThrMA== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.25.7.tgz#8c75ac9ff42c8fc0a7a5108a5d16d3539209a787" + integrity sha512-xdsLBlDCJIZzwH1fBJ7GJu+bRFO0Sqv10WotmwMu83Joep1erPcWbTr84rZD42kPzSjtmrFgshdWHKfQTWOsng== dependencies: core-js "^2.6.12" regenerator-runtime "^0.14.0" @@ -2333,15 +2455,15 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime-corejs3@^7.22.6": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.25.7.tgz#29ca319b1272e9d78faa3f7ee891d0af63c53aa2" - integrity sha512-gMmIEhg35sXk9Te5qbGp3W9YKrvLt3HV658/d3odWrHSqT0JeG5OzsJWFHRLiOohRyjRsJc/x03DhJm3i8VJxg== +"@babel/runtime-corejs3@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz#5af6bed16073eb4a0191233d61e158a5c768c430" + integrity sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w== dependencies: core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@7.22.11", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.1", "@babel/runtime@^7.24.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@7.22.11", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.25.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== @@ -2366,6 +2488,15 @@ "@babel/parser" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.7", "@babel/traverse@^7.7.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" @@ -2382,7 +2513,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.22.8", "@babel/traverse@^7.25.7": +"@babel/traverse@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8" integrity sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg== @@ -2395,6 +2526,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.23.0", "@babel/types@^7.24.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" @@ -2413,6 +2557,14 @@ "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" +"@babel/types@^7.25.9", "@babel/types@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -2428,17 +2580,15 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@^1.86.0": - version "1.86.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.86.0.tgz#389938f25b4b3f784c13dda46c3d2523a1db38cf" - integrity sha512-Ln8hC2W6d30q97Mhjboq57Qt5WhSl/2IG5Qf5VFRKha/k6Jgd4eP/gdhpUMxTlRZM3ibuQWwshsRZWCJl+VSgQ== +"@cornerstonejs/adapters@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.1.13.tgz#10f6a9d8ca72568fb8713adc7adbd01d7ef3799c" + integrity sha512-1hjDBJA707XpebfiS1u80Ebck76FQ1JwYrMET68D1rjqnpJvQLPvyaPhMgwRHpXkIlT2MF1LmSM9I3yzi0MpeQ== dependencies: "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.86.0" buffer "^6.0.3" dcmjs "^0.29.8" gl-matrix "^3.4.3" - lodash.clonedeep "^4.5.0" ndarray "^1.0.19" "@cornerstonejs/calculate-suv@^1.1.0": @@ -2466,60 +2616,35 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== -"@cornerstonejs/core@^1.83.1": - version "1.83.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.83.1.tgz#626df83e896a5e8328012c681343f1158840dfc1" - integrity sha512-VuLjcBZ+qPeUh+8RDrma7SvDGMEA/kXb1nsf+1yQTJQsqJmNiSyV+Pb7q/bn4X270KtGrEiyCJSNkb7b2zvfhQ== +"@cornerstonejs/core@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.1.13.tgz#42e176739502b0c20ca1e9e82c4285592bb13735" + integrity sha512-cywsbRizeNOCt6FrvITIhVqjBpCpf/j8s49zOJtDhzkDDLjGJNJcFpUm7QInGl9KZuW+G0E73obTZF5mIaB3sA== dependencies: - "@kitware/vtk.js" "30.4.1" + "@kitware/vtk.js" "32.1.0" comlink "^4.4.1" - detect-gpu "^5.0.22" gl-matrix "^3.4.3" - lodash.clonedeep "4.5.0" -"@cornerstonejs/core@^1.86.0": - version "1.86.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.86.0.tgz#19f94b8a4d37ffbd775b82f3963af5fbe38709de" - integrity sha512-yY3TC5abtU4hgcyBONQo+S0MXYgRyWzV/8bWujXilugAj5moDakex/zMc+YGIKlp4PvQBBRFjbu0H8YW3pseSw== - dependencies: - "@kitware/vtk.js" "30.4.1" - comlink "^4.4.1" - detect-gpu "^5.0.22" - gl-matrix "^3.4.3" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/dicom-image-loader@^1.86.0": - version "1.86.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.86.0.tgz#19a529bf1e5d27a0a6a9d0994068b12dab5e88dc" - integrity sha512-OPZCGdtdomj3TGktATQcLVIpTVv7ZqF8EyPgkEX+eIRXILzpu5Nm6gf3Lwofa5HVFwvmoJmVnU8jToKcoKPkqQ== +"@cornerstonejs/dicom-image-loader@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.1.13.tgz#133b2601748da899aac39ae557e1622104594f52" + integrity sha512-x9qpdN2d3c1b2oyTtk2FYwcBzgd46GaTOBR/7ebBvcV9jALFG5yEGKMKHPfbmd9Ej86d3UwE91MShxCfgzSVPw== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-openjpeg" "^1.2.2" "@cornerstonejs/codec-openjph" "^2.4.5" - "@cornerstonejs/core" "^1.86.0" - dicom-parser "^1.8.9" + comlink "^4.4.1" pako "^2.0.4" uuid "^9.0.0" -"@cornerstonejs/streaming-image-volume-loader@^1.82.4": - version "1.83.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.83.1.tgz#3939d754777490b48a319f4026ec5c230e646161" - integrity sha512-LcrrS1v5OOF2CkhcITAMKcoV/W3jQcCMzOl4oQHGtw6fJSSWpbQZjsIU94YnSUyhr/I0St3uTedNk5sNNzY66A== +"@cornerstonejs/tools@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.1.13.tgz#2656511fc4f4847a9b8071f52eb93f3327554292" + integrity sha512-8TAnRsB9el4oNRsCkJ1O7CpmVdvn8vWUYPyyhXfVGEje3CJ+IocK1773MmcYAP5YJ/JjBHH0d/c7O5UXHyWY3A== dependencies: - "@cornerstonejs/core" "^1.83.1" - comlink "^4.4.1" - -"@cornerstonejs/tools@^1.86.0": - version "1.86.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.86.0.tgz#ab7ca218faea4c6354866e02bcdf8005282d161c" - integrity sha512-yyy7r9Vo0NrTYYeKQtVqpbLRrVaequd9Q6aU2ifiNusdi6w+8Xslh1U/AHfw2Y9fueK8JBkLnKL+o/nLfdaOuQ== - dependencies: - "@cornerstonejs/core" "^1.86.0" - "@icr/polyseg-wasm" "0.4.0" "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1" - lodash.clonedeep "4.5.0" lodash.get "^4.4.2" "@csstools/postcss-cascade-layers@^1.1.1": @@ -2629,9 +2754,9 @@ integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== "@cypress/request@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" - integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.5.tgz#d893a6e68ce2636c085fcd8d7283c3186499ba63" + integrity sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -2639,14 +2764,14 @@ combined-stream "~1.0.6" extend "~3.0.2" forever-agent "~0.6.1" - form-data "~2.3.2" - http-signature "~1.3.6" + form-data "~4.0.0" + http-signature "~1.4.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" performance-now "^2.1.0" - qs "6.10.4" + qs "6.13.0" safe-buffer "^5.1.2" tough-cookie "^4.1.3" tunnel-agent "^0.6.0" @@ -2680,58 +2805,88 @@ "@docsearch/css" "3.6.2" algoliasearch "^4.19.1" -"@docusaurus/core@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.5.2.tgz#3adedb90e7b6104592f1231043bd6bf91680c39c" - integrity sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w== +"@docusaurus/babel@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.6.0.tgz#735a003207925bd782dd08ffa5d8b3503c1f8d72" + integrity sha512-7CsoQFiadoq7AHSUIQNkI/lGfg9AQ2ZBzsf9BqfZGXkHwWDy6twuohEaG0PgQv1npSRSAB2dioVxhRSErnqKNA== dependencies: - "@babel/core" "^7.23.3" - "@babel/generator" "^7.23.3" + "@babel/core" "^7.25.9" + "@babel/generator" "^7.25.9" "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/mdx-loader" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" - autoprefixer "^10.4.14" - babel-loader "^9.1.3" + "@babel/plugin-transform-runtime" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@babel/preset-react" "^7.25.9" + "@babel/preset-typescript" "^7.25.9" + "@babel/runtime" "^7.25.9" + "@babel/runtime-corejs3" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@docusaurus/logger" "3.6.0" + "@docusaurus/utils" "3.6.0" babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/bundler@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.6.0.tgz#bdd060ba4d009211348e4e973a3bf4861cf0996b" + integrity sha512-o5T9HXkPKH0OQAifTxEXaebcO8kaz3tU1+wlIShZ2DKJHlsyWX3N4rToWBHroWnV/ZCT2XN3kLRzXASqrnb9Tw== + dependencies: + "@babel/core" "^7.25.9" + "@docusaurus/babel" "3.6.0" + "@docusaurus/cssnano-preset" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + autoprefixer "^10.4.14" + babel-loader "^9.2.1" clean-css "^5.3.2" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" copy-webpack-plugin "^11.0.0" - core-js "^3.31.1" css-loader "^6.8.1" css-minimizer-webpack-plugin "^5.0.1" cssnano "^6.1.2" + file-loader "^6.2.0" + html-minifier-terser "^7.2.0" + mini-css-extract-plugin "^2.9.1" + null-loader "^4.0.1" + postcss "^8.4.26" + postcss-loader "^7.3.3" + react-dev-utils "^12.0.1" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.95.0" + webpackbar "^6.0.1" + +"@docusaurus/core@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.6.0.tgz#b23fc7e253a49cc3e5ac9e091354f497cc0b101b" + integrity sha512-lvRgMoKJJSRDt9+HhAqFcICV4kp/mw1cJJrLxIw4Q2XZnFGM1XUuwcbuaqWmGog+NcOLZaPCcCtZbn60EMCtjQ== + dependencies: + "@docusaurus/babel" "3.6.0" + "@docusaurus/bundler" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/mdx-loader" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + core-js "^3.31.1" del "^6.1.1" detect-port "^1.5.1" escape-html "^1.0.3" eta "^2.2.0" eval "^0.1.8" - file-loader "^6.2.0" fs-extra "^11.1.1" - html-minifier-terser "^7.2.0" html-tags "^3.3.1" - html-webpack-plugin "^5.5.3" + html-webpack-plugin "^5.6.0" leven "^3.1.0" lodash "^4.17.21" - mini-css-extract-plugin "^2.7.6" p-map "^4.0.0" - postcss "^8.4.26" - postcss-loader "^7.3.3" prompts "^2.4.2" react-dev-utils "^12.0.1" react-helmet-async "^1.3.0" @@ -2742,55 +2897,66 @@ react-router-dom "^5.3.4" rtl-detect "^1.0.4" semver "^7.5.4" - serve-handler "^6.1.5" + serve-handler "^6.1.6" shelljs "^0.8.5" - terser-webpack-plugin "^5.3.9" tslib "^2.6.0" update-notifier "^6.0.2" - url-loader "^4.1.1" - webpack "^5.88.1" - webpack-bundle-analyzer "^4.9.0" - webpack-dev-server "^4.15.1" - webpack-merge "^5.9.0" - webpackbar "^5.0.2" + webpack "^5.95.0" + webpack-bundle-analyzer "^4.10.2" + webpack-dev-server "^4.15.2" + webpack-merge "^6.0.1" -"@docusaurus/cssnano-preset@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz#6c1f2b2f9656f978c4694c84ab24592b04dcfab3" - integrity sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA== +"@docusaurus/cssnano-preset@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.6.0.tgz#02378e53e9568ed5fc8871d4fc158ea96fd7421c" + integrity sha512-h3jlOXqqzNSoU+C4CZLNpFtD+v2xr1UBf4idZpwMgqid9r6lb5GS7tWKnQnauio6OipacbHbDXEX3JyT1PlDkg== dependencies: cssnano-preset-advanced "^6.1.2" postcss "^8.4.38" postcss-sort-media-queries "^5.2.0" tslib "^2.6.0" -"@docusaurus/logger@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.5.2.tgz#1150339ad56844b30734115c19c580f3b25cf5ed" - integrity sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw== +"@docusaurus/faster@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.6.0.tgz#70540e689fd16e26e6105fe689a1347b8ac93834" + integrity sha512-9X06/KHD3f4CKm0SjZQIK3UDE/XvouAMiAlYMPV5LprubFlxVQecZG9QnfOe7VIfUh9IuPiDPuGg17yy0efwww== + dependencies: + "@rspack/core" "^1.0.14" + "@swc/core" "^1.7.39" + "@swc/html" "^1.7.39" + browserslist "^4.24.2" + lightningcss "^1.27.0" + swc-loader "^0.2.6" + tslib "^2.6.0" + webpack "^5.95.0" + +"@docusaurus/logger@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.6.0.tgz#c7349c2636087f55f573a60a3c7f69b87d59974d" + integrity sha512-BcQhoXilXW0607cH/kO6P5Gt5KxCGfoJ+QDKNf3yO2S09/RsITlW+0QljXPbI3DklTrHrhRDmgGk1yX4nUhWTA== dependencies: chalk "^4.1.2" tslib "^2.6.0" -"@docusaurus/lqip-loader@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/lqip-loader/-/lqip-loader-3.5.2.tgz#df89fc9341ffa0988f0f6d88cbbf096818842204" - integrity sha512-yUD90PgwbGciCHHiQTWXZvpLv9nVTpXrX8Ilz5Sl6oJ1bwnLgGsbl7h+EseVbwBnKhVCoujW/EKRU6+3HqeeXQ== +"@docusaurus/lqip-loader@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/lqip-loader/-/lqip-loader-3.6.0.tgz#c40cbe74690455e5c65749d4a9a2bc00806a91a9" + integrity sha512-jEQjnKAjhgyZ/aQf0Tbl3eDpp1j0C/GojE/cv2Bad89o+luV1OAOs9UHDRyKwWQfN+se0D9kFxkfsfwYxMpI8A== dependencies: - "@docusaurus/logger" "3.5.2" + "@docusaurus/logger" "3.6.0" file-loader "^6.2.0" lodash "^4.17.21" sharp "^0.32.3" tslib "^2.6.0" -"@docusaurus/mdx-loader@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz#99781641372c5037bcbe09bb8ade93a0e0ada57d" - integrity sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA== +"@docusaurus/mdx-loader@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.6.0.tgz#f8ba7af9d59473a7182f6a9307e0432f8dce905b" + integrity sha512-GhRzL1Af/AdSSrGesSPOU/iP/aXadTGmVKuysCxZDrQR2RtBtubQZ9aw+KvdFVV7R4K/CsbgD6J5oqrXlEPk3Q== dependencies: - "@docusaurus/logger" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/logger" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" "@mdx-js/mdx" "^3.0.0" "@slorber/remark-comment" "^1.0.0" escape-html "^1.0.3" @@ -2827,12 +2993,12 @@ react-helmet-async "*" react-loadable "npm:@docusaurus/react-loadable@5.5.2" -"@docusaurus/module-type-aliases@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz#4e8f9c0703e23b2e07ebfce96598ec83e4dd2a9e" - integrity sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg== +"@docusaurus/module-type-aliases@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.6.0.tgz#44083c34a53db1dde06364b4e7f2d144fa2d5394" + integrity sha512-szTrIN/6/fuk0xkf3XbRfdTFJzRQ8d1s3sQj5++58wltrT7v3yn1149oc9ryYjMpRcbsarGloQwMu7ofPe4XPg== dependencies: - "@docusaurus/types" "3.5.2" + "@docusaurus/types" "3.6.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -2840,34 +3006,34 @@ react-helmet-async "*" react-loadable "npm:@docusaurus/react-loadable@6.0.0" -"@docusaurus/plugin-client-redirects@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.5.2.tgz#ace1549e30cfca42dbc625f92920ea66a3cd7b1d" - integrity sha512-GMU0ZNoVG1DEsZlBbwLPdh0iwibrVZiRfmdppvX17SnByCVP74mb/Nne7Ss7ALgxQLtM4IHbXi8ij90VVjAJ+Q== +"@docusaurus/plugin-client-redirects@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.6.0.tgz#54155826e9e5da625e296eb93e346c11c1541a78" + integrity sha512-LIqRT6dtgxdENQH8XYwOOyxfKuzFD1ayJmIDCp9Yi/rbdcPE4vvTcESLGXOKvcyWZSfyCu+JA8Tyk4qpp+2J0w== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" eta "^2.2.0" fs-extra "^11.1.1" lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/plugin-content-blog@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz#649c07c34da7603645f152bcebdf75285baed16b" - integrity sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg== +"@docusaurus/plugin-content-blog@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.6.0.tgz#9128175b4c3ce885d9090183d74c60813844ea8d" + integrity sha512-o4aT1/E0Ldpzs/hQff5uyoSriAhS/yqBhqSn+fvSw465AaqRsva6O7CZSYleuBq6x2bewyE3QJq2PcTiHhAd8g== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/mdx-loader" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/mdx-loader" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" cheerio "1.0.0-rc.12" feed "^4.2.2" fs-extra "^11.1.1" @@ -2879,20 +3045,20 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz#adcf6c0bd9a9818eb192ab831e0069ee62d31505" - integrity sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ== +"@docusaurus/plugin-content-docs@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.6.0.tgz#15cae4bf81da0b0ddce09d53b10b7209116ea9c2" + integrity sha512-c5gZOxocJKO/Zev2MEZInli+b+VNswDGuKHE6QtFgidhAJonwjh2kwj967RvWFaMMk62HlLJLZ+IGK2XsVy4Aw== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/mdx-loader" "3.5.2" - "@docusaurus/module-type-aliases" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/mdx-loader" "3.6.0" + "@docusaurus/module-type-aliases" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" "@types/react-router-config" "^5.0.7" combine-promises "^1.1.0" fs-extra "^11.1.1" @@ -2902,139 +3068,138 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-pages@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz#2b59e43f5bc5b5176ff01835de706f1c65c2e68b" - integrity sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw== +"@docusaurus/plugin-content-pages@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.6.0.tgz#5dd284bf063baaba1e0305c90b1dd0d5acc7e466" + integrity sha512-RKHhJrfkadHc7+tt1cP48NWifOrhkSRMPdXNYytzhoQrXlP6Ph+3tfQ4/n+nT0S3Y9+wwRxYqRqA380ZLt+QtQ== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/mdx-loader" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/mdx-loader" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" fs-extra "^11.1.1" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-debug@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.5.2.tgz#c25ca6a59e62a17c797b367173fe80c06fdf2f65" - integrity sha512-kBK6GlN0itCkrmHuCS6aX1wmoWc5wpd5KJlqQ1FyrF0cLDnvsYSnh7+ftdwzt7G6lGBho8lrVwkkL9/iQvaSOA== +"@docusaurus/plugin-debug@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.6.0.tgz#0a6da9ba31a0acb176ae2762b4d6b96b1906c826" + integrity sha512-o8T1Rl94COLdSlKvjYLQpRJQRU8WWZ8EX1B0yV0dQLNN8reyH7MQW+6z1ig4sQFfH3pnjPWVGHfuEjcib5m7Eg== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" fs-extra "^11.1.1" react-json-view-lite "^1.2.0" tslib "^2.6.0" -"@docusaurus/plugin-google-analytics@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.5.2.tgz#1143e78d1461d3c74a2746f036d25b18d4a2608d" - integrity sha512-rjEkJH/tJ8OXRE9bwhV2mb/WP93V441rD6XnM6MIluu7rk8qg38iSxS43ga2V2Q/2ib53PcqbDEJDG/yWQRJhQ== +"@docusaurus/plugin-google-analytics@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.6.0.tgz#9e8245eef1bee95e44ef2af92ce3e844a8e93e64" + integrity sha512-kgRFbfpi6Hshj75YUztKyEMtI/kw0trPRwoTN4g+W1NK99R/vh8phTvhBTIMnDbetU79795LkwfG0rZ/ce6zWQ== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" tslib "^2.6.0" -"@docusaurus/plugin-google-gtag@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.5.2.tgz#60b5a9e1888c4fa16933f7c5cb5f2f2c31caad3a" - integrity sha512-lm8XL3xLkTPHFKKjLjEEAHUrW0SZBSHBE1I+i/tmYMBsjCcUB5UJ52geS5PSiOCFVR74tbPGcPHEV/gaaxFeSA== +"@docusaurus/plugin-google-gtag@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.6.0.tgz#bed8381fe3ab357d56a565f657e38d8ea6272703" + integrity sha512-nqu4IfjaO4UX+dojHL2BxHRS+sKj31CIMWYo49huQ3wTET0Oc3u/WGTaKd3ShTPDhkgiRhTOSTPUwJWrU55nHg== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" "@types/gtag.js" "^0.0.12" tslib "^2.6.0" -"@docusaurus/plugin-google-tag-manager@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.5.2.tgz#7a37334d2e7f00914d61ad05bc09391c4db3bfda" - integrity sha512-QkpX68PMOMu10Mvgvr5CfZAzZQFx8WLlOiUQ/Qmmcl6mjGK6H21WLT5x7xDmcpCoKA/3CegsqIqBR+nA137lQg== +"@docusaurus/plugin-google-tag-manager@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.6.0.tgz#326382de05888ea4317837be736eabd635adbc71" + integrity sha512-OU6c5xI0nOVbEc9eImGvvsgNWe4vGm97t/W3aLHjWsHyNk3uwFNBQMHRvBUwAi9k/K3kyC5E7DWnc67REhdLOw== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" tslib "^2.6.0" -"@docusaurus/plugin-ideal-image@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-ideal-image/-/plugin-ideal-image-3.5.2.tgz#c173f22c0e82be516b229b34f98b4f4c4424d0cd" - integrity sha512-FnHi3a5DjYRvjN1XbXRe1Cmiqfc+tAI2VmThN1Mr9teLB0ibuRi++P98q6+KyamBWKrJmuskWLMdr71acwHM8Q== +"@docusaurus/plugin-ideal-image@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-ideal-image/-/plugin-ideal-image-3.6.0.tgz#41ffed294bd18ef7f9d4bf0d2d248a4bb890d62a" + integrity sha512-6BaZU7mX+RH+lRFPm5nj3GGfAsjsJ1JHjp0Xjb4ilfte7L2IZkTl6MPpvAzyPC0qIUxfXRcJs4qd/HUE0xcTlw== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/lqip-loader" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/lqip-loader" "3.6.0" "@docusaurus/responsive-loader" "^1.7.0" - "@docusaurus/theme-translations" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/theme-translations" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" "@slorber/react-ideal-image" "^0.0.12" react-waypoint "^10.3.0" sharp "^0.32.3" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-pwa@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-pwa/-/plugin-pwa-3.5.2.tgz#ba9770c6aff3d219d6f6ac425af8274044f663cc" - integrity sha512-FCwE+C04PgoCpOnDecr4qnVJdwrOphOVRkeXSUvL6dEHjxfuB+WpSxFA6ASVSxPnFwrBhwt8UQ3vYQgxYNSstQ== +"@docusaurus/plugin-pwa@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-pwa/-/plugin-pwa-3.6.0.tgz#9851d1dd52bf8bf75f7d97e9f42cb6e29cacb68d" + integrity sha512-IVUJIBeGwbIkrI8HgVkw3eonw23wq5EjPOioXzht4Dql1Bb13xF/SmO6Bj8NE2KNmlubOKRKwLy8gYpB3i8cmQ== dependencies: - "@babel/core" "^7.23.3" - "@babel/preset-env" "^7.23.3" - "@docusaurus/core" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/theme-translations" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" - babel-loader "^9.1.3" + "@babel/core" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@docusaurus/bundler" "3.6.0" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/theme-translations" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" + babel-loader "^9.2.1" clsx "^2.0.0" core-js "^3.31.1" - terser-webpack-plugin "^5.3.9" tslib "^2.6.0" - webpack "^5.88.1" + webpack "^5.95.0" webpack-merge "^5.9.0" - webpackbar "^5.0.2" workbox-build "^7.0.0" workbox-precaching "^7.0.0" workbox-window "^7.0.0" -"@docusaurus/plugin-sitemap@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.5.2.tgz#9c940b27f3461c54d65295cf4c52cb20538bd360" - integrity sha512-DnlqYyRAdQ4NHY28TfHuVk414ft2uruP4QWCH//jzpHjqvKyXjj2fmDtI8RPUBh9K8iZKFMHRnLtzJKySPWvFA== +"@docusaurus/plugin-sitemap@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.6.0.tgz#c7c93f75f03391ca9071da48563fc4faa84966bc" + integrity sha512-YB5XMdf9FjLhgbHY/cDbYhVxsgcpPIjxY9769HUgFOB7GVzItTLOR71W035R1BiR2CA5QAn3XOSg36WLRxlhQQ== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" fs-extra "^11.1.1" sitemap "^7.1.1" tslib "^2.6.0" -"@docusaurus/preset-classic@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.5.2.tgz#977f78510bbc556aa0539149eef960bb7ab52bd9" - integrity sha512-3ihfXQ95aOHiLB5uCu+9PRy2gZCeSZoDcqpnDvf3B+sTrMvMTr8qRUzBvWkoIqc82yG5prCboRjk1SVILKx6sg== +"@docusaurus/preset-classic@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.6.0.tgz#71561f366a266be571022764eb8b9e5618f573eb" + integrity sha512-kpGNdQzr/Dpm7o3b1iaQrz4DMDx3WIeBbl4V4P4maa2zAQkTdlaP4CMgA5oKrRrpqPLnQFsUM/b+qf2glhl2Tw== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/plugin-content-blog" "3.5.2" - "@docusaurus/plugin-content-docs" "3.5.2" - "@docusaurus/plugin-content-pages" "3.5.2" - "@docusaurus/plugin-debug" "3.5.2" - "@docusaurus/plugin-google-analytics" "3.5.2" - "@docusaurus/plugin-google-gtag" "3.5.2" - "@docusaurus/plugin-google-tag-manager" "3.5.2" - "@docusaurus/plugin-sitemap" "3.5.2" - "@docusaurus/theme-classic" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/theme-search-algolia" "3.5.2" - "@docusaurus/types" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/plugin-content-blog" "3.6.0" + "@docusaurus/plugin-content-docs" "3.6.0" + "@docusaurus/plugin-content-pages" "3.6.0" + "@docusaurus/plugin-debug" "3.6.0" + "@docusaurus/plugin-google-analytics" "3.6.0" + "@docusaurus/plugin-google-gtag" "3.6.0" + "@docusaurus/plugin-google-tag-manager" "3.6.0" + "@docusaurus/plugin-sitemap" "3.6.0" + "@docusaurus/theme-classic" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/theme-search-algolia" "3.6.0" + "@docusaurus/types" "3.6.0" "@docusaurus/react-loadable@5.5.2": version "5.5.2" @@ -3044,13 +3209,13 @@ "@types/react" "*" prop-types "^15.6.2" -"@docusaurus/remark-plugin-npm2yarn@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/remark-plugin-npm2yarn/-/remark-plugin-npm2yarn-3.5.2.tgz#892a63a9b53475d7c59957bd67277656dcd91079" - integrity sha512-EAD7R/skPuhW3lWZyDhRuFs3m2EGaR008tuoe6SrFre7PifoxmSCwXk8Nb4VtVRKnnbn4IgHyqq+ma47gGmKwg== +"@docusaurus/remark-plugin-npm2yarn@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/remark-plugin-npm2yarn/-/remark-plugin-npm2yarn-3.6.0.tgz#b0e287c88ded88de576f1e2eb74d466062d73c35" + integrity sha512-Imh0v68hFWSWlcE7qLlnZD9r4bQOzM6rcWTh6mPpfx2zHAai+so+SJmrMwZ1AJGfC08Y7Ch2Q/G4AThalXRVqQ== dependencies: mdast-util-mdx "^3.0.0" - npm-to-yarn "^2.2.1" + npm-to-yarn "^3.0.0" tslib "^2.6.0" unified "^11.0.3" unist-util-visit "^5.0.0" @@ -3062,27 +3227,28 @@ dependencies: loader-utils "^2.0.0" -"@docusaurus/theme-classic@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.5.2.tgz#602ddb63d987ab1f939e3760c67bc1880f01c000" - integrity sha512-XRpinSix3NBv95Rk7xeMF9k4safMkwnpSgThn0UNQNumKvmcIYjfkwfh2BhwYh/BxMXQHJ/PdmNh22TQFpIaYg== +"@docusaurus/theme-classic@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.6.0.tgz#8f34b65c85f5082deb3633a893974d2eee309121" + integrity sha512-sAXNfwPL6uRD+BuHuKXZfAXud7SS7IK/JdrPuzyQxdO1gJKzI5GFfe1ED1QoJDNWJWJ01JHE5rSnwYLEADc2rQ== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/mdx-loader" "3.5.2" - "@docusaurus/module-type-aliases" "3.5.2" - "@docusaurus/plugin-content-blog" "3.5.2" - "@docusaurus/plugin-content-docs" "3.5.2" - "@docusaurus/plugin-content-pages" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/theme-translations" "3.5.2" - "@docusaurus/types" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/mdx-loader" "3.6.0" + "@docusaurus/module-type-aliases" "3.6.0" + "@docusaurus/plugin-content-blog" "3.6.0" + "@docusaurus/plugin-content-docs" "3.6.0" + "@docusaurus/plugin-content-pages" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/theme-translations" "3.6.0" + "@docusaurus/types" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" "@mdx-js/react" "^3.0.0" clsx "^2.0.0" copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.44" + infima "0.2.0-alpha.45" lodash "^4.17.21" nprogress "^0.2.0" postcss "^8.4.26" @@ -3093,15 +3259,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.5.2.tgz#b507ab869a1fba0be9c3c9d74f2f3d74c3ac78b2" - integrity sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew== +"@docusaurus/theme-common@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.6.0.tgz#9a061d278df76da0f70a9465cd0b7299c14d03d3" + integrity sha512-frjlYE5sRs+GuPs4XXlp9aMLI2O4H5FPpznDAXBrCm+8EpWRiIb443ePMxM3IyMCQ5bwFlki0PI9C+r4apstnw== dependencies: - "@docusaurus/mdx-loader" "3.5.2" - "@docusaurus/module-type-aliases" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" + "@docusaurus/mdx-loader" "3.6.0" + "@docusaurus/module-type-aliases" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -3111,34 +3277,34 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-live-codeblock@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.5.2.tgz#1a39505f1b221b76a0860aa8e30437f1f963e929" - integrity sha512-/jr+xvmJmvPhZsqUXQ+SGuI38qCb4dR9IZu0e+UA5my4pO63h//Nnf73naTiK3DYeszK+E0dyULPyWszVpjjOw== +"@docusaurus/theme-live-codeblock@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.6.0.tgz#de670b60c9acfdf03feb79e32a3be0d90fe11647" + integrity sha512-EphCTxJZgnEvgA1kdbr+R/cBu3fdawA2ooQji8EKPKg9i4Uex0xfFGhWja/0SaMKGqmfHRz9SY8I7j75O3cluw== dependencies: - "@docusaurus/core" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/theme-translations" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/theme-translations" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" "@philpl/buble" "^0.19.7" clsx "^2.0.0" fs-extra "^11.1.1" react-live "^4.1.6" tslib "^2.6.0" -"@docusaurus/theme-search-algolia@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.5.2.tgz#466c83ca7e8017d95ae6889ccddc5ef8bf6b61c6" - integrity sha512-qW53kp3VzMnEqZGjakaV90sst3iN1o32PH+nawv1uepROO8aEGxptcq2R5rsv7aBShSRbZwIobdvSYKsZ5pqvA== +"@docusaurus/theme-search-algolia@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.0.tgz#47dcfca68f50163abce411dd9b181855a9ec9c83" + integrity sha512-4IwRUkxjrisR8LXBHeE4d2btraWdMficbgiVL3UHvJURmyvgzMBZQP8KrK8rjdXeu8SuRxSmeV6NSVomRvdbEg== dependencies: "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.5.2" - "@docusaurus/logger" "3.5.2" - "@docusaurus/plugin-content-docs" "3.5.2" - "@docusaurus/theme-common" "3.5.2" - "@docusaurus/theme-translations" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-validation" "3.5.2" + "@docusaurus/core" "3.6.0" + "@docusaurus/logger" "3.6.0" + "@docusaurus/plugin-content-docs" "3.6.0" + "@docusaurus/theme-common" "3.6.0" + "@docusaurus/theme-translations" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-validation" "3.6.0" algoliasearch "^4.18.0" algoliasearch-helper "^3.13.3" clsx "^2.0.0" @@ -3148,10 +3314,10 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz#38f9ebf2a5d860397022206a05fef66c08863c89" - integrity sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw== +"@docusaurus/theme-translations@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.6.0.tgz#93994e931f340c1712c81ac80dbab5750c24634f" + integrity sha512-L555X8lWE3fv8VaF0Bc1VnAgi10UvRKFcvADHiYR7Gj37ItaWP5i7xLHsSw7fi/SHTXe5wfIeCFNqUYHyCOHAQ== dependencies: fs-extra "^11.1.1" tslib "^2.6.0" @@ -3175,10 +3341,10 @@ webpack "^5.88.1" webpack-merge "^5.9.0" -"@docusaurus/types@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.5.2.tgz#058019dbeffbee2d412c3f72569e412a727f9608" - integrity sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw== +"@docusaurus/types@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.6.0.tgz#8fa82332a7c7b8093b5c55e1115f5854ce484978" + integrity sha512-jADLgoZGWhAzThr+mRiyuFD4OUzt6jHnb7NRArRKorgxckqUBaPyFOau9hhbcSTHtU6ceyeWjN7FDt7uG2Hplw== dependencies: "@mdx-js/mdx" "^3.0.0" "@types/history" "^4.7.11" @@ -3187,37 +3353,37 @@ joi "^17.9.2" react-helmet-async "^1.3.0" utility-types "^3.10.0" - webpack "^5.88.1" + webpack "^5.95.0" webpack-merge "^5.9.0" -"@docusaurus/utils-common@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.5.2.tgz#4d7f5e962fbca3e2239d80457aa0e4bd3d8f7e0a" - integrity sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg== +"@docusaurus/utils-common@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.6.0.tgz#11855ea503132bbcaba6ca4d351293ff10a75d34" + integrity sha512-diUDNfbw33GaZMmKwdTckT2IBfVouXLXRD+zphH9ywswuaEIKqixvuf5g41H7MBBrlMsxhna3uTMoB4B/OPDcA== dependencies: tslib "^2.6.0" -"@docusaurus/utils-validation@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz#1b2b2f02082781cc8ce713d4c85e88d6d2fc4eb3" - integrity sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA== +"@docusaurus/utils-validation@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.6.0.tgz#5557ca14fa64ac29e6f70e61006be721395ecde5" + integrity sha512-CRHiKKJEKA0GFlfOf71JWHl7PtwOyX0+Zg9ep9NFEZv6Lcx3RJ9nhl7p8HRjPL6deyYceavM//BsfW4pCI4BtA== dependencies: - "@docusaurus/logger" "3.5.2" - "@docusaurus/utils" "3.5.2" - "@docusaurus/utils-common" "3.5.2" + "@docusaurus/logger" "3.6.0" + "@docusaurus/utils" "3.6.0" + "@docusaurus/utils-common" "3.6.0" fs-extra "^11.2.0" joi "^17.9.2" js-yaml "^4.1.0" lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/utils@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.5.2.tgz#17763130215f18d7269025903588ef7fb373e2cb" - integrity sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA== +"@docusaurus/utils@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.6.0.tgz#192785da6fd62dfd83d6f1879c3aa45547f5df23" + integrity sha512-VKczAutI4mptiAw/WcYEu5WeVhQ6Q1zdIUl64SGw9K++9lziH+Kt10Ee8l2dMpRkiUk6zzK20kMNlX2WCUwXYQ== dependencies: - "@docusaurus/logger" "3.5.2" - "@docusaurus/utils-common" "3.5.2" + "@docusaurus/logger" "3.6.0" + "@docusaurus/utils-common" "3.6.0" "@svgr/webpack" "^8.1.0" escape-string-regexp "^4.0.0" file-loader "^6.2.0" @@ -3237,16 +3403,16 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@emotion/babel-plugin@^11.11.0": - version "11.11.0" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" - integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== +"@emotion/babel-plugin@^11.12.0": + version "11.12.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" + integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== dependencies: "@babel/helper-module-imports" "^7.16.7" "@babel/runtime" "^7.18.3" - "@emotion/hash" "^0.9.1" - "@emotion/memoize" "^0.8.1" - "@emotion/serialize" "^1.1.2" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.2.0" babel-plugin-macros "^3.1.0" convert-source-map "^1.5.0" escape-string-regexp "^4.0.0" @@ -3254,21 +3420,21 @@ source-map "^0.5.7" stylis "4.2.0" -"@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": - version "11.11.0" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" - integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== +"@emotion/cache@^11.13.0", "@emotion/cache@^11.4.0": + version "11.13.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" + integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== dependencies: - "@emotion/memoize" "^0.8.1" - "@emotion/sheet" "^1.2.2" - "@emotion/utils" "^1.2.1" - "@emotion/weak-memoize" "^0.3.1" + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" stylis "4.2.0" -"@emotion/hash@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" - integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== "@emotion/is-prop-valid@^0.8.2": version "0.8.8" @@ -3282,60 +3448,60 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== -"@emotion/memoize@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" - integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== "@emotion/react@^11.8.1": - version "11.11.4" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" - integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + version "11.13.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" + integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== dependencies: "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.11.0" - "@emotion/cache" "^11.11.0" - "@emotion/serialize" "^1.1.3" - "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" - "@emotion/utils" "^1.2.1" - "@emotion/weak-memoize" "^0.3.1" + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.4.tgz#fc8f6d80c492cfa08801d544a05331d1cc7cd451" - integrity sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ== +"@emotion/serialize@^1.1.3", "@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" + integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== dependencies: - "@emotion/hash" "^0.9.1" - "@emotion/memoize" "^0.8.1" - "@emotion/unitless" "^0.8.1" - "@emotion/utils" "^1.2.1" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.1" csstype "^3.0.2" -"@emotion/sheet@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" - integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== -"@emotion/unitless@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" - integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== -"@emotion/use-insertion-effect-with-fallbacks@^1.0.0", "@emotion/use-insertion-effect-with-fallbacks@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" - integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== +"@emotion/use-insertion-effect-with-fallbacks@^1.0.0", "@emotion/use-insertion-effect-with-fallbacks@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" + integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== -"@emotion/utils@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" - integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== +"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" + integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== -"@emotion/weak-memoize@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" - integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== "@esbuild/android-arm64@0.18.20": version "0.18.20" @@ -3455,9 +3621,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.10.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0" - integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA== + version "4.11.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" + integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -3474,23 +3640,16 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== "@fal-works/esbuild-plugin-global-externals@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4" integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ== -"@floating-ui/core@^1.0.0": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a" - integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg== - dependencies: - "@floating-ui/utils" "^0.2.0" - "@floating-ui/core@^1.6.0": version "1.6.8" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" @@ -3498,15 +3657,7 @@ dependencies: "@floating-ui/utils" "^0.2.8" -"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.0.1": - version "1.6.5" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9" - integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw== - dependencies: - "@floating-ui/core" "^1.0.0" - "@floating-ui/utils" "^0.2.0" - -"@floating-ui/dom@^1.6.5": +"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.6.5": version "1.6.11" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.11.tgz#8631857838d34ee5712339eb7cbdfb8ad34da723" integrity sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ== @@ -3515,17 +3666,12 @@ "@floating-ui/utils" "^0.2.8" "@floating-ui/react-dom@^2.0.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff" - integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA== + version "2.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/utils@^0.2.0": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" - integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== - "@floating-ui/utils@^0.2.8": version "0.2.8" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" @@ -3548,12 +3694,12 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" + "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" minimatch "^3.0.5" @@ -3562,7 +3708,7 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": +"@humanwhocodes/object-schema@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== @@ -3577,7 +3723,7 @@ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== -"@icr/polyseg-wasm@0.4.0", "@icr/polyseg-wasm@^0.4.0": +"@icr/polyseg-wasm@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@icr/polyseg-wasm/-/polyseg-wasm-0.4.0.tgz#755e23d07c3d8d8fca1113278c803c1ef0185da0" integrity sha512-3sZmiwG8I0NaqPle0L7+V/ZexiR7IjIUFkUsaOoFI9rNuBGyyMMmxAxnCmqcDFtBDk9h+JEYJf6e3NnqlHi/HQ== @@ -3838,10 +3984,10 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" @@ -3856,10 +4002,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== -"@kitware/vtk.js@30.4.1": - version "30.4.1" - resolved "https://registry.yarnpkg.com/@kitware/vtk.js/-/vtk.js-30.4.1.tgz#ce8a50012e56341d2d01708a32a2ac3afa675b67" - integrity sha512-jBJFm8AyWpJjNFFBadXyvBwegdD9M6WRdxmIb+x/MVpCyA5lEZSMemhiMn71oKsznaEe5Pjv2VDVJWmwK0vhUg== +"@kitware/vtk.js@32.1.0": + version "32.1.0" + resolved "https://registry.yarnpkg.com/@kitware/vtk.js/-/vtk.js-32.1.0.tgz#db46655633b9ce8ef93a6df736fd6ee55395350f" + integrity sha512-lVLOwZtQPHCJnZ2WDXSPF8yIaGBYw97YUNFAKnQszk6G2OrHvoir3jHdUgE/Vmw0VpoXwngWumY7JY52BwXzew== dependencies: "@babel/runtime" "7.22.11" "@types/webxr" "^0.5.5" @@ -4051,6 +4197,34 @@ resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== +"@module-federation/runtime-tools@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-tools/-/runtime-tools-0.5.1.tgz#1b1f93837159a6bf0c0ba78730d589a5a8f74aa3" + integrity sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg== + dependencies: + "@module-federation/runtime" "0.5.1" + "@module-federation/webpack-bundler-runtime" "0.5.1" + +"@module-federation/runtime@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@module-federation/runtime/-/runtime-0.5.1.tgz#b548a75e2068952ff66ad717cbf73fc921edd5d7" + integrity sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ== + dependencies: + "@module-federation/sdk" "0.5.1" + +"@module-federation/sdk@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@module-federation/sdk/-/sdk-0.5.1.tgz#6c0a4053c23fa84db7aae7e4736496c541de7191" + integrity sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA== + +"@module-federation/webpack-bundler-runtime@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.5.1.tgz#ef626af0d57e3568c474d66d7d3797366e09cafd" + integrity sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA== + dependencies: + "@module-federation/runtime" "0.5.1" + "@module-federation/sdk" "0.5.1" + "@ndelangen/get-tarball@^3.0.7": version "3.0.9" resolved "https://registry.yarnpkg.com/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz#727ff4454e65f34707e742a59e5e6b1f525d8964" @@ -4388,9 +4562,9 @@ "@percy/sdk-utils" "^1.3.1" "@percy/sdk-utils@^1.3.1": - version "1.28.7" - resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.28.7.tgz#22393f76d69ffee2f4def79aa41f15a7369f7503" - integrity sha512-LIhfHnkcS0fyIdo3gvKn7rwodZjbEtyLkgiDRSRulcBOatI2mhn2Bh269sXXiiFTyAW2BDQjyE3DWc4hkGbsbQ== + version "1.30.0" + resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.30.0.tgz#0a634d530d46729606b9866e0c558a4e9d46362e" + integrity sha512-GPEexJ0gxiFXqjFpL8EtILZo/arrwdz96oyZMsCeRu9bVSQ8ZvptFlwO8hYKtXvZcY7CJfGnUyOcza8CYPxM3g== "@petamoriken/float16@^3.4.7": version "3.8.7" @@ -4464,9 +4638,9 @@ config-chain "^1.1.11" "@polka/url@^1.0.0-next.24": - version "1.0.0-next.25" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" - integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== + version "1.0.0-next.28" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" + integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== "@radix-ui/number@1.0.1": version "1.0.1" @@ -4493,15 +4667,15 @@ integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== "@radix-ui/react-accordion@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.0.tgz#aed0770fcb16285db992d81873ccd7a014c7f17d" - integrity sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.1.tgz#5c942c42c24267376b26204ec6847b17d15659b3" + integrity sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ== dependencies: "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-collapsible" "1.1.0" + "@radix-ui/react-collapsible" "1.1.1" "@radix-ui/react-collection" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" "@radix-ui/react-id" "1.1.0" "@radix-ui/react-primitive" "2.0.0" @@ -4523,29 +4697,29 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-checkbox@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz#a559c4303957d797acee99914480b755aa1f27d6" - integrity sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52" + integrity sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" -"@radix-ui/react-collapsible@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77" - integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA== +"@radix-ui/react-collapsible@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz#1382cc9ec48f8b473c14f3779d317f0cdf6da5e9" + integrity sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" @@ -4595,6 +4769,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== +"@radix-ui/react-context@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" + integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== + "@radix-ui/react-dialog@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" @@ -4617,24 +4796,24 @@ react-remove-scroll "2.5.5" "@radix-ui/react-dialog@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz#4906507f7b4ad31e22d7dad69d9330c87c431d44" - integrity sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c" + integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" - "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" "@radix-ui/react-focus-scope" "1.1.0" "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-portal" "1.1.1" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slot" "1.1.0" "@radix-ui/react-use-controllable-state" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.7" + react-remove-scroll "2.6.0" "@radix-ui/react-direction@1.0.1": version "1.0.1" @@ -4672,10 +4851,10 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-escape-keydown" "1.0.3" -"@radix-ui/react-dismissable-layer@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e" - integrity sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig== +"@radix-ui/react-dismissable-layer@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz#cbdcb739c5403382bdde5f9243042ba643883396" + integrity sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" @@ -4684,15 +4863,15 @@ "@radix-ui/react-use-escape-keydown" "1.1.0" "@radix-ui/react-dropdown-menu@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz#3dc578488688250dbbe109d9ff2ca28a9bca27ec" - integrity sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ== + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924" + integrity sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-menu" "2.1.1" + "@radix-ui/react-menu" "2.1.2" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-controllable-state" "1.1.0" @@ -4703,10 +4882,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-focus-guards@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" - integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== +"@radix-ui/react-focus-guards@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" + integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== "@radix-ui/react-focus-scope@1.0.3": version "1.0.3" @@ -4764,51 +4943,50 @@ dependencies: "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-menu@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.1.tgz#bd623ace0e1ae1ac78023a505fec0541d59fb346" - integrity sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ== +"@radix-ui/react-menu@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.2.tgz#91f6815845a4298dde775563ed2d80b7ad667899" + integrity sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-collection" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" - "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" "@radix-ui/react-focus-scope" "1.1.0" "@radix-ui/react-id" "1.1.0" "@radix-ui/react-popper" "1.2.0" - "@radix-ui/react-portal" "1.1.1" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-roving-focus" "1.1.0" "@radix-ui/react-slot" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.7" + react-remove-scroll "2.6.0" "@radix-ui/react-popover@^1.0.7": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c" - integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.2.tgz#a0cab25f69aa49ad0077d91e9e9dcd323758020c" + integrity sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-focus-guards" "1.0.1" - "@radix-ui/react-focus-scope" "1.0.4" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-popper" "1.1.3" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.5" + react-remove-scroll "2.6.0" "@radix-ui/react-popper@1.1.2": version "1.1.2" @@ -4827,23 +5005,6 @@ "@radix-ui/react-use-size" "1.0.1" "@radix-ui/rect" "1.0.1" -"@radix-ui/react-popper@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" - integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== - dependencies: - "@babel/runtime" "^7.13.10" - "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-layout-effect" "1.0.1" - "@radix-ui/react-use-rect" "1.0.1" - "@radix-ui/react-use-size" "1.0.1" - "@radix-ui/rect" "1.0.1" - "@radix-ui/react-popper@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a" @@ -4876,10 +5037,10 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-portal@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f" - integrity sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g== +"@radix-ui/react-portal@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.2.tgz#51eb46dae7505074b306ebcb985bf65cc547d74e" + integrity sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg== dependencies: "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-layout-effect" "1.1.0" @@ -4893,10 +5054,10 @@ "@radix-ui/react-compose-refs" "1.0.1" "@radix-ui/react-use-layout-effect" "1.0.1" -"@radix-ui/react-presence@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478" - integrity sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ== +"@radix-ui/react-presence@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1" + integrity sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A== dependencies: "@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" @@ -4916,22 +5077,6 @@ dependencies: "@radix-ui/react-slot" "1.1.0" -"@radix-ui/react-roving-focus@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" - integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-collection" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-controllable-state" "1.0.1" - "@radix-ui/react-roving-focus@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" @@ -4948,16 +5093,16 @@ "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-scroll-area@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5" - integrity sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg== + version "1.2.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz#d09fd693728b09c50145935bec6f91efc2661729" + integrity sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ== dependencies: "@radix-ui/number" "1.1.0" "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" @@ -4991,22 +5136,22 @@ react-remove-scroll "2.5.5" "@radix-ui/react-select@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.1.tgz#df05cb0b29d3deaef83b505917c4042e0e418a9f" - integrity sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ== + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.2.tgz#2346e118966db793940f6a866fd4cc5db2cc275e" + integrity sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA== dependencies: "@radix-ui/number" "1.1.0" "@radix-ui/primitive" "1.1.0" "@radix-ui/react-collection" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" - "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" "@radix-ui/react-focus-scope" "1.1.0" "@radix-ui/react-id" "1.1.0" "@radix-ui/react-popper" "1.2.0" - "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-portal" "1.1.2" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slot" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0" @@ -5015,17 +5160,9 @@ "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-visually-hidden" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.7" + react-remove-scroll "2.6.0" -"@radix-ui/react-separator@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa" - integrity sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" - -"@radix-ui/react-separator@^1.1.0": +"@radix-ui/react-separator@1.1.0", "@radix-ui/react-separator@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz#ee0f4d86003b0e3ea7bc6ccab01ea0adee32663e" integrity sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA== @@ -5033,15 +5170,15 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slider@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.0.tgz#7a4c817d24386b420631a3fdc75563706d743472" - integrity sha512-dAHCDA4/ySXROEPaRtaMV5WHL8+JB/DbtyTbJjYkY0RXmKMO2Ln8DFZhywG5/mVQ4WqHDBc8smc14yPXPqZHYA== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.1.tgz#acb0804309890f3cd7a224b2b0c4c4704f32921b" + integrity sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw== dependencies: "@radix-ui/number" "1.1.0" "@radix-ui/primitive" "1.1.0" "@radix-ui/react-collection" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-controllable-state" "1.1.0" @@ -5049,7 +5186,7 @@ "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" -"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": +"@radix-ui/react-slot@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== @@ -5057,7 +5194,7 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" -"@radix-ui/react-slot@1.1.0": +"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.0.2": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== @@ -5065,57 +5202,46 @@ "@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-switch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz#fcf8e778500f1d60d4b2bec2fc3fad77a7c118e3" - integrity sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.1.tgz#1401658c24d66a18610f18793afbaa7fedf5429a" + integrity sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" "@radix-ui/react-tabs@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz#0a6db1caed56776a1176aae68532060e301cc1c0" - integrity sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz#698bd97923f6bcd629738198a73beebcc4c88b30" + integrity sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw== dependencies: "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-roving-focus" "1.1.0" "@radix-ui/react-use-controllable-state" "1.1.0" -"@radix-ui/react-toggle-group@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec" - integrity sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A== +"@radix-ui/react-toggle-group@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz#28714c4d1ff4961a8fd259b1feef58b4cac92f80" + integrity sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-roving-focus" "1.0.4" - "@radix-ui/react-toggle" "1.0.3" - "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus" "1.1.0" + "@radix-ui/react-toggle" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" -"@radix-ui/react-toggle@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz#aecb2945630d1dc5c512997556c57aba894e539e" - integrity sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-controllable-state" "1.0.1" - -"@radix-ui/react-toggle@^1.1.0": +"@radix-ui/react-toggle@1.1.0", "@radix-ui/react-toggle@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz#1f7697b82917019330a16c6f96f649f46b4606cf" integrity sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw== @@ -5125,32 +5251,31 @@ "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-toolbar@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toolbar/-/react-toolbar-1.0.4.tgz#3211a105567fa016e89921b5b514877f833de559" - integrity sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q== + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toolbar/-/react-toolbar-1.1.0.tgz#a25d65b394ab6e9e7f45f4843cc0c55845bbcfab" + integrity sha512-ZUKknxhMTL/4hPh+4DuaTot9aO7UD6Kupj4gqXCsBTayX1pD1L+0C2/2VZKXb4tIifQklZ3pf2hG9T+ns+FclQ== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-roving-focus" "1.0.4" - "@radix-ui/react-separator" "1.0.3" - "@radix-ui/react-toggle-group" "1.0.4" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus" "1.1.0" + "@radix-ui/react-separator" "1.1.0" + "@radix-ui/react-toggle-group" "1.1.0" "@radix-ui/react-tooltip@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.2.tgz#c42db2ffd7dcc6ff3d65407c8cb70490288f518d" - integrity sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w== + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.3.tgz#4250b14723f2d8477e7a3d0526c169f91d1f2f74" + integrity sha512-Z4w1FIS0BqVFI2c1jZvb/uDVJijJjJ2ZMuPV81oVgTZ7g3BZxobplnMVvXtFWgtozdvYJ+MFWtwkM5S2HnAong== dependencies: "@radix-ui/primitive" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.1" "@radix-ui/react-id" "1.1.0" "@radix-ui/react-popper" "1.2.0" - "@radix-ui/react-portal" "1.1.1" - "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slot" "1.1.0" "@radix-ui/react-use-controllable-state" "1.1.0" @@ -5294,10 +5419,10 @@ resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== -"@remix-run/router@1.16.1": - version "1.16.1" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.1.tgz#73db3c48b975eeb06d0006481bde4f5f2d17d1cd" - integrity sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig== +"@remix-run/router@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273" + integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA== "@rollup/plugin-babel@^5.2.0": version "5.3.1" @@ -5365,6 +5490,86 @@ estree-walker "^2.0.2" picomatch "^2.3.1" +"@rspack/binding-darwin-arm64@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.0.14.tgz#b9d99fb71e047f5300a851614f89cb9d7168db3e" + integrity sha512-dHvlF6T6ctThGDIdvkSdacroA1xlCxfteuppBj8BX/UxzLPr4xsaEtNilfJmFfd2/J02UQyTQauN/9EBuA+YkA== + +"@rspack/binding-darwin-x64@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.0.14.tgz#ddc40886f9a0321349e6be2e9469645ae87bbe36" + integrity sha512-q4Da1Bn/4xTLhhnOkT+fjP2STsSCfp4z03/J/h8tCVG/UYz56Ud3q1UEOK33c5Fxw1C4GlhEh5yYOlSAdxFQLQ== + +"@rspack/binding-linux-arm64-gnu@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.14.tgz#2600e00adf5d1e09d7f0f476b3a01671e20731a7" + integrity sha512-JogYtL3VQS9wJ3p3FNhDqinm7avrMsdwz4erP7YCjD7idob93GYAE7dPrHUzSNVnCBYXRaHJYZHDQs7lKVcYZw== + +"@rspack/binding-linux-arm64-musl@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.14.tgz#8c2624c1426ecc51c9bd4c74c8dcc8d7b0bdbc35" + integrity sha512-qgybhxI/nnoa8CUz7zKTC0Oh37NZt9uRxsSV7+ZYrfxqbrVCoNVuutPpY724uUHy1M6W34kVEm1uT1N4Ka5cZg== + +"@rspack/binding-linux-x64-gnu@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.14.tgz#6636bf658304d246d617f01ae12f4cbe29097a62" + integrity sha512-5vzaDRw3/sGKo3ax/1cU3/cxqNjajwlt2LU288vXNe1/n8oe/pcDfYcTugpOe/A1DqzadanudJszLpFcKsaFtQ== + +"@rspack/binding-linux-x64-musl@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.14.tgz#887c7f26876495f45842ebab874198db394dfd6f" + integrity sha512-4U6QD9xVS1eGme52DuJr6Fg/KdcUfJ+iKwH49Up460dZ/fLvGylnVGA+V0mzPlKi8gfy7NwFuYXZdu3Pwi1YYg== + +"@rspack/binding-win32-arm64-msvc@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.14.tgz#cfde74d44a866ed42501d107349a56b7b0122042" + integrity sha512-SjeYw7qqRHYZ5RPClu+ffKZsShQdU3amA1OwC3M0AS6dbfEcji8482St3Y8Z+QSzYRapCEZij9LMM/9ypEhISg== + +"@rspack/binding-win32-ia32-msvc@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.14.tgz#59532613cea22aa78928fb92b1a35347d780aaab" + integrity sha512-m1gUiVyz3Z3VYIK/Ayo5CVHBjnEeRk9a+KIpKSsq1yhZItnMgjtr4bKabU9vjxalO4UoaSmVzODJI8lJBlnn5Q== + +"@rspack/binding-win32-x64-msvc@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.14.tgz#5bf023ba319dc748e54f8c23bcecab79703dc163" + integrity sha512-Gbeg+bayMF9VP9xmlxySL/TC2XrS6/LZM/pqcNOTLHx6LMG/VXCcmKB0rOZo8MzLXEt8D/lQmQ/B6g7pSaAw0g== + +"@rspack/binding@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/binding/-/binding-1.0.14.tgz#60a04aca4369f7c4ca646e69cbeee75636a418ef" + integrity sha512-0wWqFvr9hkF4LgNPgWfkTU0hhkZAMvOytoCs2p+wDX1Up1E/SgJ1U1JAsCxsl1XtUKm7mRvdWHzJmHbza3y89Q== + optionalDependencies: + "@rspack/binding-darwin-arm64" "1.0.14" + "@rspack/binding-darwin-x64" "1.0.14" + "@rspack/binding-linux-arm64-gnu" "1.0.14" + "@rspack/binding-linux-arm64-musl" "1.0.14" + "@rspack/binding-linux-x64-gnu" "1.0.14" + "@rspack/binding-linux-x64-musl" "1.0.14" + "@rspack/binding-win32-arm64-msvc" "1.0.14" + "@rspack/binding-win32-ia32-msvc" "1.0.14" + "@rspack/binding-win32-x64-msvc" "1.0.14" + +"@rspack/core@^1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@rspack/core/-/core-1.0.14.tgz#7b7305391a488b5ac5dc28a82e9fb02b7ec3e8de" + integrity sha512-xHl23lxJZNjItGc5YuE9alz3yjb56y7EgJmAcBMPHMqgjtUt8rBu4xd/cSUjbr9/lLF9N4hdyoJiPJOFs9LEjw== + dependencies: + "@module-federation/runtime-tools" "0.5.1" + "@rspack/binding" "1.0.14" + "@rspack/lite-tapable" "1.0.1" + caniuse-lite "^1.0.30001616" + +"@rspack/lite-tapable@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz#d4540a5d28bd6177164bc0ba0bee4bdec0458591" + integrity sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w== + +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -5466,141 +5671,141 @@ micromark-util-character "^1.1.0" micromark-util-symbol "^1.0.1" -"@storybook/addon-actions@7.6.19", "@storybook/addon-actions@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-7.6.19.tgz#f131faf51c2baf036aa0fff33d77a1fa74e22ad0" - integrity sha512-ATLrA5QKFJt7tIAScRHz5T3eBQ+RG3jaZk08L7gChvyQZhei8knWwePElZ7GaWbCr9BgznQp1lQUUXq/UUblAQ== +"@storybook/addon-actions@7.6.20", "@storybook/addon-actions@^7.6.10": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-7.6.20.tgz#4264d1fba6e889f28f717ebb23c55b7d774a2f60" + integrity sha512-c/GkEQ2U9BC/Ew/IMdh+zvsh4N6y6n7Zsn2GIhJgcu9YEAa5aF2a9/pNgEGBMOABH959XE8DAOMERw/5qiLR8g== dependencies: - "@storybook/core-events" "7.6.19" + "@storybook/core-events" "7.6.20" "@storybook/global" "^5.0.0" "@types/uuid" "^9.0.1" dequal "^2.0.2" polished "^4.2.2" uuid "^9.0.0" -"@storybook/addon-backgrounds@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.19.tgz#cd4cf6a6415c32a0e813573553efa592400c6c2f" - integrity sha512-Nu3LAZODRSV2e5bOroKm/Jp6BIFzwu/nJxD5OvLWkkwNCh+vDXUFbbaVrZf5xRL+fHd9iLFPtWbJQpF/w7UsCw== +"@storybook/addon-backgrounds@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.20.tgz#a84758c07b236181f2d67966a7c159d0b3bc1abb" + integrity sha512-a7ukoaXT42vpKsMxkseIeO3GqL0Zst2IxpCTq5dSlXiADrcemSF/8/oNpNW9C4L6F1Zdt+WDtECXslEm017FvQ== dependencies: "@storybook/global" "^5.0.0" memoizerific "^1.11.3" ts-dedent "^2.0.0" -"@storybook/addon-controls@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-7.6.19.tgz#86c0433b3d5e2e74f6ca3e58e492730d27f0220f" - integrity sha512-cl6PCNEwihDjuWIUsKTyDNKk+/IE4J3oMbSY5AZV/9Z0jJbpMV2shVm5DMZm5LhCCVcu5obWcxCIa4FMIMJAMQ== +"@storybook/addon-controls@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-7.6.20.tgz#5487064259a71f10b0aab04a4b7745ecf948e4cc" + integrity sha512-06ZT5Ce1sZW52B0s6XuokwjkKO9GqHlTUHvuflvd8wifxKlCmRvNUxjBvwh+ccGJ49ZS73LbMSLFgtmBEkCxbg== dependencies: - "@storybook/blocks" "7.6.19" + "@storybook/blocks" "7.6.20" lodash "^4.17.21" ts-dedent "^2.0.0" -"@storybook/addon-docs@7.6.19", "@storybook/addon-docs@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-7.6.19.tgz#4f0866072a5105e667ed75bd388584bb46206884" - integrity sha512-nv+9SR/NOtM8Od2esOXHcg0NQT8Pk8BMUyGwZu5Q3MLI4JxNVEG65dY0IP2j6Knc4UtlvQTpM0f7m5xp4seHjQ== +"@storybook/addon-docs@7.6.20", "@storybook/addon-docs@^7.6.10": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-7.6.20.tgz#0bff85bdbdca58c9535384a4ded69dadb2fe7e4e" + integrity sha512-XNfYRhbxH5JP7B9Lh4W06PtMefNXkfpV39Gaoih5HuqngV3eoSL4RikZYOMkvxRGQ738xc6axySU3+JKcP1OZg== dependencies: "@jest/transform" "^29.3.1" "@mdx-js/react" "^2.1.5" - "@storybook/blocks" "7.6.19" - "@storybook/client-logger" "7.6.19" - "@storybook/components" "7.6.19" - "@storybook/csf-plugin" "7.6.19" - "@storybook/csf-tools" "7.6.19" + "@storybook/blocks" "7.6.20" + "@storybook/client-logger" "7.6.20" + "@storybook/components" "7.6.20" + "@storybook/csf-plugin" "7.6.20" + "@storybook/csf-tools" "7.6.20" "@storybook/global" "^5.0.0" "@storybook/mdx2-csf" "^1.0.0" - "@storybook/node-logger" "7.6.19" - "@storybook/postinstall" "7.6.19" - "@storybook/preview-api" "7.6.19" - "@storybook/react-dom-shim" "7.6.19" - "@storybook/theming" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/node-logger" "7.6.20" + "@storybook/postinstall" "7.6.20" + "@storybook/preview-api" "7.6.20" + "@storybook/react-dom-shim" "7.6.20" + "@storybook/theming" "7.6.20" + "@storybook/types" "7.6.20" fs-extra "^11.1.0" remark-external-links "^8.0.0" remark-slug "^6.0.0" ts-dedent "^2.0.0" "@storybook/addon-essentials@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-7.6.19.tgz#1f2a7af9388e1f50b3cfd2fd9ee5fd763a1ac35a" - integrity sha512-SC33ZEQ5YaOt9wDkrdZmwQgqPWo9om/gqnyif06eug3SwrTe9JjO5iq1PIBfQodLD9MAxr9cwBvO0NG505oszQ== + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-7.6.20.tgz#149c22b51a7abd8977acaaf2e1941c5b5dcb2fd5" + integrity sha512-hCupSOiJDeOxJKZSgH0x5Mb2Xqii6mps21g5hpxac1XjhQtmGflShxi/xOHhK3sNqrbgTSbScfpUP3hUlZO/2Q== dependencies: - "@storybook/addon-actions" "7.6.19" - "@storybook/addon-backgrounds" "7.6.19" - "@storybook/addon-controls" "7.6.19" - "@storybook/addon-docs" "7.6.19" - "@storybook/addon-highlight" "7.6.19" - "@storybook/addon-measure" "7.6.19" - "@storybook/addon-outline" "7.6.19" - "@storybook/addon-toolbars" "7.6.19" - "@storybook/addon-viewport" "7.6.19" - "@storybook/core-common" "7.6.19" - "@storybook/manager-api" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/preview-api" "7.6.19" + "@storybook/addon-actions" "7.6.20" + "@storybook/addon-backgrounds" "7.6.20" + "@storybook/addon-controls" "7.6.20" + "@storybook/addon-docs" "7.6.20" + "@storybook/addon-highlight" "7.6.20" + "@storybook/addon-measure" "7.6.20" + "@storybook/addon-outline" "7.6.20" + "@storybook/addon-toolbars" "7.6.20" + "@storybook/addon-viewport" "7.6.20" + "@storybook/core-common" "7.6.20" + "@storybook/manager-api" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/preview-api" "7.6.20" ts-dedent "^2.0.0" -"@storybook/addon-highlight@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-highlight/-/addon-highlight-7.6.19.tgz#7e88fe924e822426ef54c33ad1522a9676ef57aa" - integrity sha512-/pApl0oiVU1CQ8xETRNDLDthMBjeTmvFnTRq8RJ9m0JYTrSsoyHDmj9zS4K1k9gReqijE7brslhP8d2tblBpNw== +"@storybook/addon-highlight@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-highlight/-/addon-highlight-7.6.20.tgz#d118e4cce549238d866bbbe4d49b9509afda01a7" + integrity sha512-7/x7xFdFyqCki5Dm3uBePldUs9l98/WxJ7rTHQuYqlX7kASwyN5iXPzuhmMRUhlMm/6G6xXtLabIpzwf1sFurA== dependencies: "@storybook/global" "^5.0.0" "@storybook/addon-links@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-7.6.19.tgz#7b48683533ada47268a60393fb1930415e67113f" - integrity sha512-qMIFfcsMf4olxhYUHUV2ZJhxphh6Xpf1DMd0lxKqAibfxl/sX1m0rJkyiqWSBxbCmAy/pwdgqEOJ1lpDUsJ33w== + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-7.6.20.tgz#c6bedc7bdc0112ce4cb3f1bfc701445df696598d" + integrity sha512-iomSnBD90CA4MinesYiJkFX2kb3P1Psd/a1Y0ghlFEsHD4uMId9iT6sx2s16DYMja0SlPkrbWYnGukqaCjZpRw== dependencies: "@storybook/csf" "^0.1.2" "@storybook/global" "^5.0.0" ts-dedent "^2.0.0" -"@storybook/addon-measure@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-7.6.19.tgz#4f12580eff40038a8d514be1059b2d063e5d0ed4" - integrity sha512-n+cfhVXXouBv9oQr3a77vvip5dTznaNoBDWMafP2ohauc8jBlAxeBwCjk5r3pyThMRIFCTG/ypZrhiJcSJT3bw== +"@storybook/addon-measure@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-7.6.20.tgz#c764009ce3e980b5b67e462ad0de5986c38cdfab" + integrity sha512-i2Iq08bGfI7gZbG6Lb8uF/L287tnaGUR+2KFEmdBjH6+kgjWLiwfpanoPQpy4drm23ar0gUjX+L3Ri03VI5/Xg== dependencies: "@storybook/global" "^5.0.0" tiny-invariant "^1.3.1" -"@storybook/addon-outline@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-7.6.19.tgz#519cdba12d3f27f1282897d2abe8762a5640b45f" - integrity sha512-Tt4MrfjK5j/Mdh8nJ8ccVyh78Dy7aiEPxO31YVvr5XUkge0pDi1PX328mHRDPur0i56NM8ssVbekWBZr+9MxlA== +"@storybook/addon-outline@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-7.6.20.tgz#0ebe829b6d8d269f691a110f3b34884b1df8ee74" + integrity sha512-TdsIQZf/TcDsGoZ1XpO+9nBc4OKqcMIzY4SrI8Wj9dzyFLQ37s08gnZr9POci8AEv62NTUOVavsxcafllkzqDQ== dependencies: "@storybook/global" "^5.0.0" ts-dedent "^2.0.0" -"@storybook/addon-toolbars@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-7.6.19.tgz#4800d5d8679334b4a3353bb7e4933af5a6545d4b" - integrity sha512-+qGbPP2Vo/HoPiS4EJopZ127HGculCV74Hkz6ot7ob6AkYdA1yLMPzWns/ZXNIWm6ab3jV+iq+mQCM/i1qJzvA== +"@storybook/addon-toolbars@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-7.6.20.tgz#c1cd31c6a8f98d3ec4853157134ca143d065d31a" + integrity sha512-5Btg4i8ffWTDHsU72cqxC8nIv9N3E3ObJAc6k0llrmPBG/ybh3jxmRfs8fNm44LlEXaZ5qrK/petsXX3UbpIFg== -"@storybook/addon-viewport@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-7.6.19.tgz#602aa2c0431f4b8119f9cf4e668eb3155ad5bcb7" - integrity sha512-OQQtJ2kYwImbvE9QiC3I3yR0O0EBgNjq+XSaSS4ixJrvUyesfuB7Lm7RkubhEEiP4yANi9OlbzsqZelmPOnk6w== +"@storybook/addon-viewport@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-7.6.20.tgz#882571f4b0f405e1cf2cfad9a1f74b30d22f9a93" + integrity sha512-i8mIw8BjLWAVHEQsOTE6UPuEGQvJDpsu1XZnOCkpfTfPMz73m+3td/PmLG7mMT2wPnLu9IZncKLCKTAZRbt/YQ== dependencies: memoizerific "^1.11.3" -"@storybook/blocks@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/blocks/-/blocks-7.6.19.tgz#11bd3126245be33df091b48df9f25c1c07920825" - integrity sha512-/c/bVQRmyRPoviJhPrFdLfubRcrnZWTwkjxsCvrOTJ/UDOyEl0t/H8yY1mGq7KWWTdbIznnZWhAIofHnH4/Esw== +"@storybook/blocks@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/blocks/-/blocks-7.6.20.tgz#1cc142f1c238616f0f3a9f900965c651e7ee7c52" + integrity sha512-xADKGEOJWkG0UD5jbY4mBXRlmj2C+CIupDL0/hpzvLvwobxBMFPKZIkcZIMvGvVnI/Ui+tJxQxLSuJ5QsPthUw== dependencies: - "@storybook/channels" "7.6.19" - "@storybook/client-logger" "7.6.19" - "@storybook/components" "7.6.19" - "@storybook/core-events" "7.6.19" + "@storybook/channels" "7.6.20" + "@storybook/client-logger" "7.6.20" + "@storybook/components" "7.6.20" + "@storybook/core-events" "7.6.20" "@storybook/csf" "^0.1.2" - "@storybook/docs-tools" "7.6.19" + "@storybook/docs-tools" "7.6.20" "@storybook/global" "^5.0.0" - "@storybook/manager-api" "7.6.19" - "@storybook/preview-api" "7.6.19" - "@storybook/theming" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/manager-api" "7.6.20" + "@storybook/preview-api" "7.6.20" + "@storybook/theming" "7.6.20" + "@storybook/types" "7.6.20" "@types/lodash" "^4.14.167" color-convert "^2.0.1" dequal "^2.0.2" @@ -5614,15 +5819,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-manager@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/builder-manager/-/builder-manager-7.6.19.tgz#1356fab233181a06c8cea9094b53c84aac218bae" - integrity sha512-Dt5OLh97xeWh4h2mk9uG0SbCxBKHPhIiHLHAKEIDzIZBdwUhuyncVNDPHW2NlXM+S7U0/iKs2tw05waqh2lHvg== +"@storybook/builder-manager@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/builder-manager/-/builder-manager-7.6.20.tgz#d550a3f209012e4e383e61320ea756cddfdb416e" + integrity sha512-e2GzpjLaw6CM/XSmc4qJRzBF8GOoOyotyu3JrSPTYOt4RD8kjUsK4QlismQM1DQRu8i39aIexxmRbiJyD74xzQ== dependencies: "@fal-works/esbuild-plugin-global-externals" "^2.1.2" - "@storybook/core-common" "7.6.19" - "@storybook/manager" "7.6.19" - "@storybook/node-logger" "7.6.19" + "@storybook/core-common" "7.6.20" + "@storybook/manager" "7.6.20" + "@storybook/node-logger" "7.6.20" "@types/ejs" "^3.1.1" "@types/find-cache-dir" "^3.2.1" "@yarnpkg/esbuild-plugin-pnp" "^3.0.0-rc.10" @@ -5636,20 +5841,20 @@ process "^0.11.10" util "^0.12.4" -"@storybook/builder-webpack5@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-7.6.19.tgz#1e2ff6328ead8d1fa0fef6aa552ee8c6d1d434ee" - integrity sha512-PeP66orYG0tWoWeOGNcCDKtk/kpDBFfosViCkd0Pxb6c2MtvjOuHSGWGB/9AI3hjodsoe5p9xo/SqGf7lDzpoA== +"@storybook/builder-webpack5@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-7.6.20.tgz#416bb22db997f206cca6da657c43ce2749675a0b" + integrity sha512-kUcMZHVo/jybwsje03MFN1ZucdjyH6QB+jlw9dzHrAhM6N1IItwHzhlixvxmseA5OB7jk1b0WcCN8tfD2qByFA== dependencies: "@babel/core" "^7.23.2" - "@storybook/channels" "7.6.19" - "@storybook/client-logger" "7.6.19" - "@storybook/core-common" "7.6.19" - "@storybook/core-events" "7.6.19" - "@storybook/core-webpack" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/preview" "7.6.19" - "@storybook/preview-api" "7.6.19" + "@storybook/channels" "7.6.20" + "@storybook/client-logger" "7.6.20" + "@storybook/core-common" "7.6.20" + "@storybook/core-events" "7.6.20" + "@storybook/core-webpack" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/preview" "7.6.20" + "@storybook/preview-api" "7.6.20" "@swc/core" "^1.3.82" "@types/node" "^18.0.0" "@types/semver" "^7.3.4" @@ -5680,35 +5885,35 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.5.0" -"@storybook/channels@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-7.6.19.tgz#730fa74f7800e2069707f8a880996ca6fc8957ab" - integrity sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA== +"@storybook/channels@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-7.6.20.tgz#33d8292b1b16d7f504bf751c57a792477d1c3a9e" + integrity sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A== dependencies: - "@storybook/client-logger" "7.6.19" - "@storybook/core-events" "7.6.19" + "@storybook/client-logger" "7.6.20" + "@storybook/core-events" "7.6.20" "@storybook/global" "^5.0.0" qs "^6.10.0" telejson "^7.2.0" tiny-invariant "^1.3.1" -"@storybook/cli@7.6.19", "@storybook/cli@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-7.6.19.tgz#b4f36ccd51e02ceadb92c3e2341f82ee1bbd6598" - integrity sha512-7OVy7nPgkLfgivv6/dmvoyU6pKl9EzWFk+g9izyQHiM/jS8jOiEyn6akG8Ebj6k5pWslo5lgiXUSW+cEEZUnqQ== +"@storybook/cli@7.6.20", "@storybook/cli@^7.6.10": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-7.6.20.tgz#498625db5f2447e8e1ad34827a7803c5940527f0" + integrity sha512-ZlP+BJyqg7HlnXf7ypjG2CKMI/KVOn03jFIiClItE/jQfgR6kRFgtjRU7uajh427HHfjv9DRiur8nBzuO7vapA== dependencies: "@babel/core" "^7.23.2" "@babel/preset-env" "^7.23.2" "@babel/types" "^7.23.0" "@ndelangen/get-tarball" "^3.0.7" - "@storybook/codemod" "7.6.19" - "@storybook/core-common" "7.6.19" - "@storybook/core-events" "7.6.19" - "@storybook/core-server" "7.6.19" - "@storybook/csf-tools" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/telemetry" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/codemod" "7.6.20" + "@storybook/core-common" "7.6.20" + "@storybook/core-events" "7.6.20" + "@storybook/core-server" "7.6.20" + "@storybook/csf-tools" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/telemetry" "7.6.20" + "@storybook/types" "7.6.20" "@types/semver" "^7.3.4" "@yarnpkg/fslib" "2.10.3" "@yarnpkg/libzip" "2.3.0" @@ -5738,25 +5943,25 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.6.19.tgz#a6f91af8cdc640ace9903674b6340ad8173238cc" - integrity sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ== +"@storybook/client-logger@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.6.20.tgz#1d6e93443091cccd50e269371aa786172d0c4659" + integrity sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ== dependencies: "@storybook/global" "^5.0.0" -"@storybook/codemod@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-7.6.19.tgz#ac6690a5ef18903cdb132a005b3c520f96ae92f8" - integrity sha512-bmHE0iEEgWZ65dXCmasd+GreChjPiWkXu2FEa0cJmNz/PqY12GsXGls4ke1TkNTj4gdSZnbtJxbclPZZnib2tQ== +"@storybook/codemod@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-7.6.20.tgz#0aa7e0c1aacc605c7691b4b06baef0a9abefe114" + integrity sha512-8vmSsksO4XukNw0TmqylPmk7PxnfNfE21YsxFa7mnEBmEKQcZCQsNil4ZgWfG0IzdhTfhglAN4r++Ew0WE+PYA== dependencies: "@babel/core" "^7.23.2" "@babel/preset-env" "^7.23.2" "@babel/types" "^7.23.0" "@storybook/csf" "^0.1.2" - "@storybook/csf-tools" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/csf-tools" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/types" "7.6.20" "@types/cross-spawn" "^6.0.2" cross-spawn "^7.0.3" globby "^11.0.2" @@ -5765,38 +5970,38 @@ prettier "^2.8.0" recast "^0.23.1" -"@storybook/components@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-7.6.19.tgz#c9e36100faef6310455daf5bf915a21447c332d1" - integrity sha512-8Zw/RQ4crzKkUR7ojxvRIj8vktKiBBO8Nq93qv4JfDqDWrcR7cro0hOlZgmZmrzbFunBBt6WlsNNO6nVP7R4Xw== +"@storybook/components@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-7.6.20.tgz#09d044923142d2e087a1c4a43dec6731a42d2871" + integrity sha512-0d8u4m558R+W5V+rseF/+e9JnMciADLXTpsILrG+TBhwECk0MctIWW18bkqkujdCm8kDZr5U2iM/5kS1Noy7Ug== dependencies: "@radix-ui/react-select" "^1.2.2" "@radix-ui/react-toolbar" "^1.0.4" - "@storybook/client-logger" "7.6.19" + "@storybook/client-logger" "7.6.20" "@storybook/csf" "^0.1.2" "@storybook/global" "^5.0.0" - "@storybook/theming" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/theming" "7.6.20" + "@storybook/types" "7.6.20" memoizerific "^1.11.3" use-resize-observer "^9.1.0" util-deprecate "^1.0.2" -"@storybook/core-client@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-7.6.19.tgz#ca47b71bfebb0e1e5cb112b5b88e9f7b0c88eee4" - integrity sha512-F0V9nzcEnj6DIpnw2ilrxsV4d9ibyyQS+Wi2uQtXy+wCQQm9PeBVqrOywjXAY2F9pcoftXOaepfhp8jrxX4MXw== +"@storybook/core-client@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-7.6.20.tgz#831681d64194e4d604a859ed3eb452981f6824c5" + integrity sha512-upQuQQinLmlOPKcT8yqXNtwIucZ4E4qegYZXH5HXRWoLAL6GQtW7sUVSIuFogdki8OXRncr/dz8OA+5yQyYS4w== dependencies: - "@storybook/client-logger" "7.6.19" - "@storybook/preview-api" "7.6.19" + "@storybook/client-logger" "7.6.20" + "@storybook/preview-api" "7.6.20" -"@storybook/core-common@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-7.6.19.tgz#fdbfc3c5b24d12e74ef9d1f0e15cd4b1ee2cd02c" - integrity sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q== +"@storybook/core-common@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-7.6.20.tgz#3a2a3ae570bd13dc34726178c0eb36cf6a64e2a4" + integrity sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw== dependencies: - "@storybook/core-events" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/core-events" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/types" "7.6.20" "@types/find-cache-dir" "^3.2.1" "@types/node" "^18.0.0" "@types/node-fetch" "^2.6.4" @@ -5818,33 +6023,33 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/core-events@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.6.19.tgz#cfa7d4581ad6cff1ee7eeade31d602a7d879d2b7" - integrity sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg== +"@storybook/core-events@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.6.20.tgz#6648d661d1c96841a4c2a710a35759b01b6a06a1" + integrity sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ== dependencies: ts-dedent "^2.0.0" -"@storybook/core-server@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.6.19.tgz#07f4f0d4b05dd7dc52e9c60be97df7b5cb3150a9" - integrity sha512-7mKL73Wv5R2bEl0kJ6QJ9bOu5YY53Idu24QgvTnUdNsQazp2yUONBNwHIrNDnNEXm8SfCi4Mc9o0mmNRMIoiRA== +"@storybook/core-server@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.6.20.tgz#fa143fbcad64fb7b0f0dc6d555d083c506a44ab4" + integrity sha512-qC5BdbqqwMLTdCwMKZ1Hbc3+3AaxHYWLiJaXL9e8s8nJw89xV8c8l30QpbJOGvcDmsgY6UTtXYaJ96OsTr7MrA== dependencies: "@aw-web-design/x-default-browser" "1.4.126" "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-manager" "7.6.19" - "@storybook/channels" "7.6.19" - "@storybook/core-common" "7.6.19" - "@storybook/core-events" "7.6.19" + "@storybook/builder-manager" "7.6.20" + "@storybook/channels" "7.6.20" + "@storybook/core-common" "7.6.20" + "@storybook/core-events" "7.6.20" "@storybook/csf" "^0.1.2" - "@storybook/csf-tools" "7.6.19" + "@storybook/csf-tools" "7.6.20" "@storybook/docs-mdx" "^0.1.0" "@storybook/global" "^5.0.0" - "@storybook/manager" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/preview-api" "7.6.19" - "@storybook/telemetry" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/manager" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/preview-api" "7.6.20" + "@storybook/telemetry" "7.6.20" + "@storybook/types" "7.6.20" "@types/detect-port" "^1.3.0" "@types/node" "^18.0.0" "@types/pretty-hrtime" "^1.0.0" @@ -5857,7 +6062,6 @@ express "^4.17.3" fs-extra "^11.1.0" globby "^11.0.2" - ip "^2.0.1" lodash "^4.17.21" open "^8.4.0" pretty-hrtime "^1.0.3" @@ -5872,44 +6076,44 @@ watchpack "^2.2.0" ws "^8.2.3" -"@storybook/core-webpack@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/core-webpack/-/core-webpack-7.6.19.tgz#bf1bdcc6899c21c8a3c7702737dd5e1847380dbb" - integrity sha512-Ezvn54hFN99qwP8kDOQa7/IEk2V3NyJys2eg0Afqz1cy9Uc3SkL7U7hQorKOHr5+66dsryNDfJdPzM1YMKFMBQ== +"@storybook/core-webpack@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/core-webpack/-/core-webpack-7.6.20.tgz#f821061f55210e35a8aeda0e3ba2cf75e5510451" + integrity sha512-pGYhKQhMYQ76HPL336L5n7eiJGk1sjWFkA+xRRRmQ9q6VUlqtEPuRHjKBQwrrTb1nA33BQX58Be06OtlbsFkjg== dependencies: - "@storybook/core-common" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/core-common" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/types" "7.6.20" "@types/node" "^18.0.0" ts-dedent "^2.0.0" -"@storybook/csf-plugin@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-7.6.19.tgz#5c88767f6b4f47826c9fc7fdf028132ff243239b" - integrity sha512-yUP0xfJyR8e6fmCgKoEt4c1EvslF8dZ8wtwVLE5hnC3kfs7xt8RVDiKLB/9NhYjY3mD/oOesX60HqRXDgJQHwA== +"@storybook/csf-plugin@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-7.6.20.tgz#0e79e58d5ed47dfb472b1dc202b0e754c21ec33b" + integrity sha512-dzBzq0dN+8WLDp6NxYS4G7BCe8+vDeDRBRjHmM0xb0uJ6xgQViL8SDplYVSGnk3bXE/1WmtvyRzQyTffBnaj9Q== dependencies: - "@storybook/csf-tools" "7.6.19" + "@storybook/csf-tools" "7.6.20" unplugin "^1.3.1" -"@storybook/csf-tools@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-7.6.19.tgz#5925f313b8ac4ebdd1c0b9d71279e18bbaab269a" - integrity sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ== +"@storybook/csf-tools@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-7.6.20.tgz#fdd9fa9459720a627e83e31d3839721dbc655f22" + integrity sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ== dependencies: "@babel/generator" "^7.23.0" "@babel/parser" "^7.23.0" "@babel/traverse" "^7.23.2" "@babel/types" "^7.23.0" "@storybook/csf" "^0.1.2" - "@storybook/types" "7.6.19" + "@storybook/types" "7.6.20" fs-extra "^11.1.0" recast "^0.23.1" ts-dedent "^2.0.0" "@storybook/csf@^0.1.2": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.8.tgz#63a83dc493c462d84e0f333e3f3264d319bec716" - integrity sha512-Ntab9o7LjBCbFIao5l42itFiaSh/Qu+l16l/r/9qmV9LnYZkO+JQ7tzhdlwpgJfhs+B5xeejpdAtftDRyXNajw== + version "0.1.11" + resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.11.tgz#ad685a4fe564a47a6b73571c2e7c07b526f4f71b" + integrity sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg== dependencies: type-fest "^2.19.0" @@ -5918,14 +6122,14 @@ resolved "https://registry.yarnpkg.com/@storybook/docs-mdx/-/docs-mdx-0.1.0.tgz#33ba0e39d1461caf048b57db354b2cc410705316" integrity sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg== -"@storybook/docs-tools@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-7.6.19.tgz#d63b68af1d425f4b94a3a229af611bbaf822f4b4" - integrity sha512-JuwV6wtm7Hb7Kb5ValChfxy4J7XngfrSQNpvwsDCSBNVcQUv2y843hvclpa26Ptfr/c7zpUX8r9FGSaMDy+2aQ== +"@storybook/docs-tools@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-7.6.20.tgz#2a6dd402c880e24ec6bec8411beee89cfe69f932" + integrity sha512-Bw2CcCKQ5xGLQgtexQsI1EGT6y5epoFzOINi0FSTGJ9Wm738nRp5LH3dLk1GZLlywIXcYwOEThb2pM+pZeRQxQ== dependencies: - "@storybook/core-common" "7.6.19" - "@storybook/preview-api" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/core-common" "7.6.20" + "@storybook/preview-api" "7.6.20" + "@storybook/types" "7.6.20" "@types/doctrine" "^0.0.3" assert "^2.1.0" doctrine "^3.0.0" @@ -5936,19 +6140,19 @@ resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== -"@storybook/manager-api@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-7.6.19.tgz#d08787dabe4143cf34d5805a023499889d572032" - integrity sha512-dVCx1Q+HZEA4U08XqYljiG88BeS3I3ahnPAQLZAeWQXQRkoc9G2jMgLNPKYPIqEtq7Xrn6SRlFMIofhwWrwZpg== +"@storybook/manager-api@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-7.6.20.tgz#225ff7dea3dbdb2e82bb5568babdaace4071c32e" + integrity sha512-gOB3m8hO3gBs9cBoN57T7jU0wNKDh+hi06gLcyd2awARQlAlywnLnr3s1WH5knih6Aq+OpvGBRVKkGLOkaouCQ== dependencies: - "@storybook/channels" "7.6.19" - "@storybook/client-logger" "7.6.19" - "@storybook/core-events" "7.6.19" + "@storybook/channels" "7.6.20" + "@storybook/client-logger" "7.6.20" + "@storybook/core-events" "7.6.20" "@storybook/csf" "^0.1.2" "@storybook/global" "^5.0.0" - "@storybook/router" "7.6.19" - "@storybook/theming" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/router" "7.6.20" + "@storybook/theming" "7.6.20" + "@storybook/types" "7.6.20" dequal "^2.0.2" lodash "^4.17.21" memoizerific "^1.11.3" @@ -5956,38 +6160,38 @@ telejson "^7.2.0" ts-dedent "^2.0.0" -"@storybook/manager@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/manager/-/manager-7.6.19.tgz#d7ee419e48feed79c1d9f0f469efa7742a2ef3d2" - integrity sha512-fZWQcf59x4P0iiBhrL74PZrqKJAPuk9sWjP8BIkGbf8wTZtUunbY5Sv4225fOL4NLJbuX9/RYLUPoxQ3nucGHA== +"@storybook/manager@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/manager/-/manager-7.6.20.tgz#eb619fe8d33446e581a7b1c3050644c196364d39" + integrity sha512-0Cf6WN0t7yEG2DR29tN5j+i7H/TH5EfPppg9h9/KiQSoFHk+6KLoy2p5do94acFU+Ro4+zzxvdCGbcYGKuArpg== "@storybook/mdx2-csf@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz#97f6df04d0bf616991cc1005a073ac004a7281e5" integrity sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw== -"@storybook/node-logger@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-7.6.19.tgz#8a911288ee8052cf2c77cf5e2db2367b1b852b43" - integrity sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA== +"@storybook/node-logger@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-7.6.20.tgz#c0ca90cf68cf31d84cdcf53c76cec22769407ece" + integrity sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw== -"@storybook/postinstall@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-7.6.19.tgz#8f75f43ea786a26a8677dedbedc5e8f947c56456" - integrity sha512-s6p1vpgMfn+QGDfCK2YNdyyWKidUgb3nGicB81FANRyzYqGB//QlJlghEc2LKCIQbGIZQiwP3l8PdZQmczEJRw== +"@storybook/postinstall@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-7.6.20.tgz#5a77ce7913375b11bd7c72388798854bd8507b91" + integrity sha512-AN4WPeNma2xC2/K/wP3I/GMbBUyeSGD3+86ZFFJFO1QmE/Zea6E+1aVlTd1iKHQUcNkZ9bZTrqkhPGVYx10pIw== -"@storybook/preset-react-webpack@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/preset-react-webpack/-/preset-react-webpack-7.6.19.tgz#c79e1e3727175012fe6f2ac92a58c19426a4dd3f" - integrity sha512-WvfDE4upH7jmisx5XOn4E07p9Fm8YJn4Aywc9vYM1jqQ8A1lEH8VSC1KR6dPfdmGr94jRscQkD6fjs9sUNTdrw== +"@storybook/preset-react-webpack@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/preset-react-webpack/-/preset-react-webpack-7.6.20.tgz#6d535c77139a29d698964efa990363e7949aa1f9" + integrity sha512-z5/NF+HI9zN/ONocNyxQwewaG5G/1ChCeWfi5m5E1mwKQxxJbFUgE8oiAFhe90A1R7lAEsGFKd8WxdefY2JvEg== dependencies: "@babel/preset-flow" "^7.22.15" "@babel/preset-react" "^7.22.15" "@pmmmwh/react-refresh-webpack-plugin" "^0.5.11" - "@storybook/core-webpack" "7.6.19" - "@storybook/docs-tools" "7.6.19" - "@storybook/node-logger" "7.6.19" - "@storybook/react" "7.6.19" + "@storybook/core-webpack" "7.6.20" + "@storybook/docs-tools" "7.6.20" + "@storybook/node-logger" "7.6.20" + "@storybook/react" "7.6.20" "@storybook/react-docgen-typescript-plugin" "1.0.6--canary.9.0c3f3b7.0" "@types/node" "^18.0.0" "@types/semver" "^7.3.4" @@ -5999,17 +6203,17 @@ semver "^7.3.7" webpack "5" -"@storybook/preview-api@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.6.19.tgz#5f4b36489a7662e4671030dc2580cc11a1dd854e" - integrity sha512-04hdMSQucroJT4dBjQzRd7ZwH2hij8yx2nm5qd4HYGkd1ORkvlH6GOLph4XewNJl5Um3xfzFQzBhvkqvG0WaCQ== +"@storybook/preview-api@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.6.20.tgz#688a435ee2cfe57eeb1e3053c18025a9e0a03bbb" + integrity sha512-3ic2m9LDZEPwZk02wIhNc3n3rNvbi7VDKn52hDXfAxnL5EYm7yDICAkaWcVaTfblru2zn0EDJt7ROpthscTW5w== dependencies: - "@storybook/channels" "7.6.19" - "@storybook/client-logger" "7.6.19" - "@storybook/core-events" "7.6.19" + "@storybook/channels" "7.6.20" + "@storybook/client-logger" "7.6.20" + "@storybook/core-events" "7.6.20" "@storybook/csf" "^0.1.2" "@storybook/global" "^5.0.0" - "@storybook/types" "7.6.19" + "@storybook/types" "7.6.20" "@types/qs" "^6.9.5" dequal "^2.0.2" lodash "^4.17.21" @@ -6019,10 +6223,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/preview@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/preview/-/preview-7.6.19.tgz#4cb2b2e1730aea8612c07d80a7908835f9806265" - integrity sha512-VqRPua2koOQTOteB+VvuKNXFYQ7IDEopaPpj9Nx+3kom+bqp0hWdAysWcm6CtKN2GGzBQm+5PvGibMNdawsaVg== +"@storybook/preview@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/preview/-/preview-7.6.20.tgz#df39739dce6e183efaf06a8c15a9459f019e631b" + integrity sha512-cxYlZ5uKbCYMHoFpgleZqqGWEnqHrk5m5fT8bYSsDsdQ+X5wPcwI/V+v8dxYAdQcMphZVIlTjo6Dno9WG8qmVA== "@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0": version "1.0.6--canary.9.0c3f3b7.0" @@ -6037,33 +6241,33 @@ react-docgen-typescript "^2.2.2" tslib "^2.0.0" -"@storybook/react-dom-shim@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-7.6.19.tgz#c6472be7c53b8d9d3ed3007f5fc5d41daa76ac09" - integrity sha512-tpt2AC1428d1gF4fetMkpkeFZ1WdDr1CLKoLbSInWQZ7i96nbnIMIA9raR/W8ai1bo55KSz9Bq5ytC/1Pac2qQ== +"@storybook/react-dom-shim@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-7.6.20.tgz#20b902663474b731c22b211ec29c7fd0e86b4b7f" + integrity sha512-SRvPDr9VWcS24ByQOVmbfZ655y5LvjXRlsF1I6Pr9YZybLfYbu3L5IicfEHT4A8lMdghzgbPFVQaJez46DTrkg== "@storybook/react-webpack5@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/react-webpack5/-/react-webpack5-7.6.19.tgz#efd4ba044d3d18020ef60a7c352080d0773bee3e" - integrity sha512-QPnDv5eimvgc0zBIvc1H49iBUfZhs0hhrs9eO0+rAC6sIo5BiDcX9nQJZEuamRPVuLjqoRByj6vUpqGI25zASg== + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/react-webpack5/-/react-webpack5-7.6.20.tgz#812e798cfff142c870ea12347b6a2ba2c270502d" + integrity sha512-xaLtadKczfUdpyPMk/e49qGnRpjMDtTwFq4RqkS7q+Z+EO72kTCUPGtK3jJXyv70pp/qbzM5OfjFLjXjMezvYw== dependencies: - "@storybook/builder-webpack5" "7.6.19" - "@storybook/preset-react-webpack" "7.6.19" - "@storybook/react" "7.6.19" + "@storybook/builder-webpack5" "7.6.20" + "@storybook/preset-react-webpack" "7.6.20" + "@storybook/react" "7.6.20" "@types/node" "^18.0.0" -"@storybook/react@7.6.19", "@storybook/react@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-7.6.19.tgz#a15b41fc98a80086b2f40d025f34f8641b3dea8b" - integrity sha512-uKShAAp1/pRki1YnRjBveH/jAD3f8V0W2WP1LxTQqnKVFkl01mTbDZ/9ZIK6rVTSILUlmsk3fwsNyRbOKVgBGQ== +"@storybook/react@7.6.20", "@storybook/react@^7.6.10": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-7.6.20.tgz#e326969851b96e9a6bea6fdd81f624de052ddbb6" + integrity sha512-i5tKNgUbTNwlqBWGwPveDhh9ktlS0wGtd97A1ZgKZc3vckLizunlAFc7PRC1O/CMq5PTyxbuUb4RvRD2jWKwDA== dependencies: - "@storybook/client-logger" "7.6.19" - "@storybook/core-client" "7.6.19" - "@storybook/docs-tools" "7.6.19" + "@storybook/client-logger" "7.6.20" + "@storybook/core-client" "7.6.20" + "@storybook/docs-tools" "7.6.20" "@storybook/global" "^5.0.0" - "@storybook/preview-api" "7.6.19" - "@storybook/react-dom-shim" "7.6.19" - "@storybook/types" "7.6.19" + "@storybook/preview-api" "7.6.20" + "@storybook/react-dom-shim" "7.6.20" + "@storybook/types" "7.6.20" "@types/escodegen" "^0.0.6" "@types/estree" "^0.0.51" "@types/node" "^18.0.0" @@ -6079,56 +6283,56 @@ type-fest "~2.19" util-deprecate "^1.0.2" -"@storybook/router@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-7.6.19.tgz#b9805344d35bb00c9139787f7c561603ffe0d3c2" - integrity sha512-q2/AvY8rG0znFEfbg50OIhkS5yQ6OmyzdCdztoEsDDdsbq87YPmsDj7k8Op1EkTa2T5CB8XhBOCQDtcj7gUUtg== +"@storybook/router@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-7.6.20.tgz#ffa6a3ba1790e86f1d2364c27d3511f7975742a6" + integrity sha512-mCzsWe6GrH47Xb1++foL98Zdek7uM5GhaSlrI7blWVohGa0qIUYbfJngqR4ZsrXmJeeEvqowobh+jlxg3IJh+w== dependencies: - "@storybook/client-logger" "7.6.19" + "@storybook/client-logger" "7.6.20" memoizerific "^1.11.3" qs "^6.10.0" "@storybook/source-loader@^7.6.10": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-7.6.19.tgz#5d910b4874f7bfb121d854c7921e777ee7df2a6e" - integrity sha512-Pbbq2RrS0Ct7e1MXVKW62a61Wh4iZQ66LWwksZKqW0bMM4Ohdw8v+OZI00y7Wk4+/u1gbUDplQk2k0JlueOrMw== + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-7.6.20.tgz#ce559aad34093a361bc8266d9c8d30079761083c" + integrity sha512-AlOX5w95tajmZEsEcbYCtwtpKYW0xLV2hRzk2MZfc6XUXC9/n2jwhz1X4EKilwKGCWFUj/HAYbNc2XChS9juvg== dependencies: "@storybook/csf" "^0.1.2" - "@storybook/types" "7.6.19" + "@storybook/types" "7.6.20" estraverse "^5.2.0" lodash "^4.17.21" prettier "^2.8.0" -"@storybook/telemetry@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-7.6.19.tgz#c0e11bea942057fd8eef66680c39e0cd4e0d9970" - integrity sha512-rA5xum4I36M57iiD3uzmW0MOdpl0vEpHWBSAa5hK0a0ALPeY9TgAsQlI/0dSyNYJ/K7aczEEN6d4qm1NC4u10A== +"@storybook/telemetry@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-7.6.20.tgz#5b3705eb5100b21070d76767dde1040ed5d9b35b" + integrity sha512-dmAOCWmOscYN6aMbhCMmszQjoycg7tUPRVy2kTaWg6qX10wtMrvEtBV29W4eMvqdsoRj5kcvoNbzRdYcWBUOHQ== dependencies: - "@storybook/client-logger" "7.6.19" - "@storybook/core-common" "7.6.19" - "@storybook/csf-tools" "7.6.19" + "@storybook/client-logger" "7.6.20" + "@storybook/core-common" "7.6.20" + "@storybook/csf-tools" "7.6.20" chalk "^4.1.0" detect-package-manager "^2.0.1" fetch-retry "^5.0.2" fs-extra "^11.1.0" read-pkg-up "^7.0.1" -"@storybook/theming@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-7.6.19.tgz#f5032d74d5c0cf5f7c7a389a0b9d2d3bc5e62a25" - integrity sha512-sAho13MmtA80ctOaLn8lpkQBsPyiqSdLcOPH5BWFhatQzzBQCpTAKQk+q/xGju8bNiPZ+yQBaBzbN8SfX8ceCg== +"@storybook/theming@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-7.6.20.tgz#c932cd82c27314979d22d0e7867268e301f5f97c" + integrity sha512-iT1pXHkSkd35JsCte6Qbanmprx5flkqtSHC6Gi6Umqoxlg9IjiLPmpHbaIXzoC06DSW93hPj5Zbi1lPlTvRC7Q== dependencies: "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" - "@storybook/client-logger" "7.6.19" + "@storybook/client-logger" "7.6.20" "@storybook/global" "^5.0.0" memoizerific "^1.11.3" -"@storybook/types@7.6.19": - version "7.6.19" - resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.6.19.tgz#ec73c9afb6003c57e260e1709441af4db9f50190" - integrity sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg== +"@storybook/types@7.6.20": + version "7.6.20" + resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.6.20.tgz#b8d62b30914b35e6750b1f4937da532432f02890" + integrity sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q== dependencies: - "@storybook/channels" "7.6.19" + "@storybook/channels" "7.6.20" "@types/babel__core" "^7.0.0" "@types/express" "^4.7.0" file-system-cache "2.3.0" @@ -6352,84 +6556,228 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@swc/core-darwin-arm64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.1.tgz#72d861fb7094b7a0004f4f300e2c5d4ea1549d9e" - integrity sha512-u6GdwOXsOEdNAdSI6nWq6G2BQw5HiSNIZVcBaH1iSvBnxZvWbnIKyDiZKaYnDwTLHLzig2GuUjjE2NaCJPy4jg== +"@swc/core-darwin-arm64@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz#5f4096c00e71771ca1b18c824f0c92a052c70760" + integrity sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw== -"@swc/core-darwin-x64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.6.1.tgz#8b7070fcee4a4570d0af245c4614ca4e492dfd5b" - integrity sha512-/tXwQibkDNLVbAtr7PUQI0iQjoB708fjhDDDfJ6WILSBVZ3+qs/LHjJ7jHwumEYxVq1XA7Fv2Q7SE/ZSQoWHcQ== +"@swc/core-darwin-arm64@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.40.tgz#1e51a2e6c360d5839c30006583ba4e5d42d77927" + integrity sha512-LRRrCiRJLb1kpQtxMNNsr5W82Inr0dy5Imho+4HQzVx/Ismi0qX4hQBgzJAnyOBNLK1+OBVb/912UVhKXppdfQ== -"@swc/core-linux-arm-gnueabihf@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.1.tgz#bea6d2e75127bbc65a664284f012ffa90c8325d5" - integrity sha512-aDgipxhJTms8iH78emHVutFR2c16LNhO+NTRCdYi+X4PyIn58/DyYTH6VDZ0AeEcS5f132ZFldU5AEgExwihXA== +"@swc/core-darwin-x64@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz#867b7a4f094e6b64201090ca5fcbf3da7d0f3e22" + integrity sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ== -"@swc/core-linux-arm64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.1.tgz#5c84d804ec23cf54b31c0bc0b4bdd30ec5d43ce8" - integrity sha512-XkJ+eO4zUKG5g458RyhmKPyBGxI0FwfWFgpfIj5eDybxYJ6s4HBT5MoxyBLorB5kMlZ0XoY/usUMobPVY3nL0g== +"@swc/core-darwin-x64@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.7.40.tgz#a79ef324618ebde757bb21ba06751f06f026b822" + integrity sha512-Lpl0XK/4fLzS5jsK48opUuGXrqJXwqJckYYPwyGbCfCXm4MsBe+7dX2hq/Kc4YMY25+NeTmzAXhla8TT4WYD/g== -"@swc/core-linux-arm64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.1.tgz#e167a350bec12caebc97304068c3ffbad6c398ce" - integrity sha512-dr6YbLBg/SsNxs1hDqJhxdcrS8dGMlOXJwXIrUvACiA8jAd6S5BxYCaqsCefLYXtaOmu0bbx1FB/evfodqB70Q== +"@swc/core-linux-arm-gnueabihf@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz#35bb43894def296d92aaa2cc9372d48042f37777" + integrity sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q== -"@swc/core-linux-x64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.1.tgz#fdd4e1d63b3e53d195e2ddcb9cb5ad9f31995796" - integrity sha512-A0b/3V+yFy4LXh3O9umIE7LXPC7NBWdjl6AQYqymSMcMu0EOb1/iygA6s6uWhz9y3e172Hpb9b/CGsuD8Px/bg== +"@swc/core-linux-arm-gnueabihf@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.40.tgz#81da8373a5fac134a21f2b06070d1921742e301e" + integrity sha512-4bEvvjptpoc5BRPr/R419h6fXTEuub+frpxxlxBOEKxgXjAF/S3xdxyPijUAakmW/xXBF0u7OC4KYI+38yQp6g== -"@swc/core-linux-x64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.1.tgz#81a312dd9e62da5f4c48e3cd23b6c6d28a31ac42" - integrity sha512-5dJjlzZXhC87nZZZWbpiDP8kBIO0ibis893F/rtPIQBI5poH+iJuA32EU3wN4/WFHeK4et8z6SGSVghPtWyk4g== +"@swc/core-linux-arm64-gnu@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz#8e2321cc4ec84cbfed8f8e16ff1ed7b854450443" + integrity sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q== -"@swc/core-win32-arm64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.1.tgz#e131f579a69c5d807013e54ccb311e10caa27bcb" - integrity sha512-HBi1ZlwvfcUibLtT3g/lP57FaDPC799AD6InolB2KSgkqyBbZJ9wAXM8/CcH67GLIP0tZ7FqblrJTzGXxetTJQ== +"@swc/core-linux-arm64-gnu@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.40.tgz#33b13bf2251de47c694ac554f189a3bfebfc09f9" + integrity sha512-v2fBlHJ/6Ovz0L2xFAI9TRiKyl9DTdx139PuAHD9gyzp16Utl/W0MPd4t2cYdkI6hPXE9PsJCSzMOrduh+YoDg== -"@swc/core-win32-ia32-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.1.tgz#9f3d88cf0e826aa8222a695177a065ed2899eb21" - integrity sha512-AKqHohlWERclexar5y6ux4sQ8yaMejEXNxeKXm7xPhXrp13/1p4/I3E5bPVX/jMnvpm4HpcKSP0ee2WsqmhhPw== +"@swc/core-linux-arm64-musl@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz#b1c16e4b23ffa9ff19973eda6ffee35d2a7de7b0" + integrity sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg== -"@swc/core-win32-x64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.1.tgz#b2082710bc46c484a2c9f2e33a15973806e5031d" - integrity sha512-0dLdTLd+ONve8kgC5T6VQ2Y5G+OZ7y0ujjapnK66wpvCBM6BKYGdT/OKhZKZydrC5gUKaxFN6Y5oOt9JOFUrOQ== +"@swc/core-linux-arm64-musl@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.40.tgz#9bd2bd051081e75be1af7dc56fcbb8e6ab4042f7" + integrity sha512-uMkduQuU4LFVkW6txv8AVArT8GjJVJ5IHoWloXaUBMT447iE8NALmpePdZWhMyj6KV7j0y23CM5rzV/I2eNGLg== + +"@swc/core-linux-x64-gnu@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz#388e2cc13a010cd28787aead2cecf31eb491836d" + integrity sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w== + +"@swc/core-linux-x64-gnu@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.40.tgz#384fa2578f0f5bfc5022884004654919034dbea9" + integrity sha512-4LZdY1MBSnXyTpW5fpBU/+JGAhkuHT+VnFTDNegRboN5nSPh7y0Yvn4LmIioESV+sWzjKkEXujJPGjrp+oSp5w== + +"@swc/core-linux-x64-musl@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz#51e0ff30981f26d7a5b97a7a7b5b291bad050d1a" + integrity sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ== + +"@swc/core-linux-x64-musl@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.40.tgz#49464ad222234620c7b15e8ee755efcca1822a90" + integrity sha512-FPjOwT3SgI6PAwH1O8bhOGBPzuvzOlzKeCtxLaCjruHJu9V8KKBrMTWOZT/FJyYC9mX5Ip1+l9j30UqUZdQxtA== + +"@swc/core-win32-arm64-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz#a7fdcc4074c34ee6a026506b594d00323383c11f" + integrity sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA== + +"@swc/core-win32-arm64-msvc@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.40.tgz#eca19f24bb5997d0cb22655fca533b7b35fc141e" + integrity sha512-//ovXdD9GsTmhPmXJlXnIbRQkeuL6PSrYSr7uCMNcclrUdJG0YkO0GMM2afUKYbdJcunylDDWsSS8PFWn0QxmA== + +"@swc/core-win32-ia32-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz#ae7be6dde798eebee2000b8fd84e01a439b5bd6a" + integrity sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ== + +"@swc/core-win32-ia32-msvc@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.40.tgz#92affe2556ee1bdb576263dcc2f42192991d735a" + integrity sha512-iD/1auVhHGlhWAPrWmfRWL3w4AvXIWGVXZiSA109/xnRIPiHKb/HqqTp/qB94E/ZHMPRgLKkLTNwamlkueUs8g== + +"@swc/core-win32-x64-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz#310d607004d7319085a4dec20c0c38c3405cc05b" + integrity sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w== + +"@swc/core-win32-x64-msvc@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.40.tgz#4fe5405f8a16db5bb4222fa6ba34856ecb053fcc" + integrity sha512-ZlFAV1WFPhhWQ/8esiygmetkb905XIcMMtHRRG0FBGCllO+HVL5nikUaLDgTClz1onmEY9sMXUFQeoPtvliV+w== "@swc/core@^1.3.82": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.6.1.tgz#a899a205cfaa8e23f805451ef4787987e03b8920" - integrity sha512-Yz5uj5hNZpS5brLtBvKY0L4s2tBAbQ4TjmW8xF1EC3YLFxQRrUjMP49Zm1kp/KYyYvTkSaG48Ffj2YWLu9nChw== + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.7.26.tgz#beda9b82063fcec7b56c958804a4d175aecf9a9d" + integrity sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw== dependencies: "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.8" + "@swc/types" "^0.1.12" optionalDependencies: - "@swc/core-darwin-arm64" "1.6.1" - "@swc/core-darwin-x64" "1.6.1" - "@swc/core-linux-arm-gnueabihf" "1.6.1" - "@swc/core-linux-arm64-gnu" "1.6.1" - "@swc/core-linux-arm64-musl" "1.6.1" - "@swc/core-linux-x64-gnu" "1.6.1" - "@swc/core-linux-x64-musl" "1.6.1" - "@swc/core-win32-arm64-msvc" "1.6.1" - "@swc/core-win32-ia32-msvc" "1.6.1" - "@swc/core-win32-x64-msvc" "1.6.1" + "@swc/core-darwin-arm64" "1.7.26" + "@swc/core-darwin-x64" "1.7.26" + "@swc/core-linux-arm-gnueabihf" "1.7.26" + "@swc/core-linux-arm64-gnu" "1.7.26" + "@swc/core-linux-arm64-musl" "1.7.26" + "@swc/core-linux-x64-gnu" "1.7.26" + "@swc/core-linux-x64-musl" "1.7.26" + "@swc/core-win32-arm64-msvc" "1.7.26" + "@swc/core-win32-ia32-msvc" "1.7.26" + "@swc/core-win32-x64-msvc" "1.7.26" + +"@swc/core@^1.7.39": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.7.40.tgz#f77fee1fb1f4ab4446fd54e2ea282a46dfa49070" + integrity sha512-0HIzM5vigVT5IvNum+pPuST9p8xFhN6mhdIKju7qYYeNuZG78lwms/2d8WgjTJJlzp6JlPguXGrMMNzjQw0qNg== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.13" + optionalDependencies: + "@swc/core-darwin-arm64" "1.7.40" + "@swc/core-darwin-x64" "1.7.40" + "@swc/core-linux-arm-gnueabihf" "1.7.40" + "@swc/core-linux-arm64-gnu" "1.7.40" + "@swc/core-linux-arm64-musl" "1.7.40" + "@swc/core-linux-x64-gnu" "1.7.40" + "@swc/core-linux-x64-musl" "1.7.40" + "@swc/core-win32-arm64-msvc" "1.7.40" + "@swc/core-win32-ia32-msvc" "1.7.40" + "@swc/core-win32-x64-msvc" "1.7.40" "@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/types@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.8.tgz#2c81d107c86cfbd0c3a05ecf7bb54c50dfa58a95" - integrity sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA== +"@swc/html-darwin-arm64@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.7.40.tgz#f3132d864cc0a0e111d084549283cded693eed6e" + integrity sha512-QQRmyrRXvr0RDu4GeaDBA3u0EB6WXOVWUkDdUhmgZh1MLlwfxvTg9FtjlFxUa9ls9dq2STGZEmhok8y4qrA69A== + +"@swc/html-darwin-x64@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.7.40.tgz#a3fa91e9c2257baf10d358ed3d82edb7b888165d" + integrity sha512-DVYK2omr6WjsVFhuKz+4NuqufAF15/+Ne4mv6GJYFzHhdjQirXGfg+RdxaX2P91E2zIAoXLnmha0v8q8NiGSuw== + +"@swc/html-linux-arm-gnueabihf@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.7.40.tgz#c046d54cb7fbac180dfbb4fffb683c4141e6d183" + integrity sha512-NAj8Jyb5DEY03DrhIA0mE4kx26jkNgzS7IVCcMs96OYMORAeGEgrsokmtWbNu9UVKJfWbjSzkYVut2hZBlpI8w== + +"@swc/html-linux-arm64-gnu@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.7.40.tgz#566a1c3214fa6f52436565b780456d1639f4cea1" + integrity sha512-V/24oGNQDLVTi4i7kQtyeOfGqm/g+ypQrW/NNQaMAaiju2aPCfCD+mBz1wo2M/MhGIQP8MIDAhU48rzKCWdDYw== + +"@swc/html-linux-arm64-musl@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.7.40.tgz#5d19d7e89248ff4bf158153c81461c1c7923c67d" + integrity sha512-gyDZJJupzyj4GncDhEzQ4g3LZNmuE2dT+oYSKmerB9FfdxHbTK/LBQ1T11p7iV4GEqGBAu5gy8TYZXn48uFtAw== + +"@swc/html-linux-x64-gnu@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.7.40.tgz#8cbc269a5a4227b044c7c306ccb6155447821b93" + integrity sha512-uqx6YnwZPP5J+9JW0sqSEZMT9en/BQF3o1XcBPXiRQ07BWQNTe5nsm4XRMEA4l7bQgdh3ZeJydV0OPecErJSmA== + +"@swc/html-linux-x64-musl@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.7.40.tgz#dd3ae22273c4c7cdb649e307cfca67a348805559" + integrity sha512-WQt851yaae1iuDvbZR+h0qx+r7Hq/aGi8xQO8X/GlCwlleoBYrwWa802Fn5+hZc2W7dYSepWRbIehsv9n05PjQ== + +"@swc/html-win32-arm64-msvc@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.7.40.tgz#2e371ed749bee70cd7e7d39cf4c9e052d12c422d" + integrity sha512-jKMiJW0F/MdyjuQ1/m2cpBVKhI7oaIhmXQVV/pp0hmtm8RBxuVzeF46c7x7ef2kftcnvRyYbKnEFOFs7Vp6PeA== + +"@swc/html-win32-ia32-msvc@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.7.40.tgz#7dda55d2571526e1d582c04674a9060b7676aa7b" + integrity sha512-5eEFNpanozjm1aZVWegywCWDnoH4NVcP4acQTFttakTVByE7S2MuhXWxc+fZvMd6ePE05L5k9Ed25zSrbeedag== + +"@swc/html-win32-x64-msvc@1.7.40": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.7.40.tgz#fa2d4a8eec6bb6d007fd43f40300bf0971df5f8e" + integrity sha512-YrE1vWvK9QKqqPEeQ/0TwOgg0SgpGmCt4nTeU048/cQuVWfYOb6n/XeIJDk9bCCNx0wGIkyEpG7e0OSe9Bq3fQ== + +"@swc/html@^1.7.39": + version "1.7.40" + resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.7.40.tgz#aed93db0e7ae836baa21d65e8ef8ccf499b06538" + integrity sha512-hDhYb2K7sEz474j6xCUjq1flVt1fY38t8BVI0tYL+JgiJ/Cm05Fk1U7IIzp/Z5J1s2QTipW9iGx62JawFHwZEQ== + dependencies: + "@swc/counter" "^0.1.3" + optionalDependencies: + "@swc/html-darwin-arm64" "1.7.40" + "@swc/html-darwin-x64" "1.7.40" + "@swc/html-linux-arm-gnueabihf" "1.7.40" + "@swc/html-linux-arm64-gnu" "1.7.40" + "@swc/html-linux-arm64-musl" "1.7.40" + "@swc/html-linux-x64-gnu" "1.7.40" + "@swc/html-linux-x64-musl" "1.7.40" + "@swc/html-win32-arm64-msvc" "1.7.40" + "@swc/html-win32-ia32-msvc" "1.7.40" + "@swc/html-win32-x64-msvc" "1.7.40" + +"@swc/types@^0.1.12": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.12.tgz#7f632c06ab4092ce0ebd046ed77ff7557442282f" + integrity sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA== + dependencies: + "@swc/counter" "^0.1.3" + +"@swc/types@^0.1.13": + version "0.1.13" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.13.tgz#441734f8bfa6e9e738f1c68e98be6da282ecc7db" + integrity sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q== dependencies: "@swc/counter" "^0.1.3" @@ -6649,17 +6997,37 @@ dependencies: "@types/node" "*" -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.19.3" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a" - integrity sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c" + integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/send" "*" -"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.7.0": +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.13", "@types/express@^4.7.0": version "4.17.21" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== @@ -6722,9 +7090,9 @@ integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== "@types/http-proxy@^1.17.8": - version "1.17.14" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" - integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== dependencies: "@types/node" "*" @@ -6783,9 +7151,9 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/lodash@^4.14.167": - version "4.17.5" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.5.tgz#e6c29b58e66995d57cd170ce3e2a61926d55ee04" - integrity sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw== + version "4.17.10" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6" + integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ== "@types/mdast@^3.0.0": version "3.0.15" @@ -6852,11 +7220,11 @@ "@types/node" "*" "@types/node@*": - version "20.14.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420" - integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg== + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== dependencies: - undici-types "~5.26.4" + undici-types "~6.19.2" "@types/node@^11.9.4": version "11.15.54" @@ -6869,18 +7237,18 @@ integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== "@types/node@^18.0.0": - version "18.19.36" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.36.tgz#c9861e84727e07ecf79a5ff6d0e14f91bab2b478" - integrity sha512-tX1BNmYSWEvViftB26VLNxT6mEr37M7+ldUtq7rlKnv4/2fKYsJIOmqJAjT6h1DNuwQjIKgw3VJ/Dtw3yiTIQw== + version "18.19.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.55.tgz#29c3f8e1485a92ec96636957ddec55aabc6e856e" + integrity sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw== dependencies: undici-types "~5.26.4" "@types/node@^20.12.12": - version "20.14.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.5.tgz#fe35e3022ebe58b8f201580eb24e1fcfc0f2487d" - integrity sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA== + version "20.16.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.11.tgz#9b544c3e716b1577ac12e70f9145193f32750b33" + integrity sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw== dependencies: - undici-types "~5.26.4" + undici-types "~6.19.2" "@types/normalize-package-data@^2.4.0": version "2.4.4" @@ -6908,9 +7276,9 @@ integrity sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg== "@types/prop-types@*": - version "15.7.12" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" - integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== "@types/q@^1.5.1": version "1.5.8" @@ -6918,9 +7286,9 @@ integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== "@types/qs@*", "@types/qs@^6.9.5": - version "6.9.15" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" - integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + version "6.9.16" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" + integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== "@types/range-parser@*": version "1.2.7" @@ -6961,9 +7329,9 @@ "@types/react" "*" "@types/react-transition-group@^4.4.0": - version "4.4.10" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" - integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== + version "4.4.11" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" + integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== dependencies: "@types/react" "*" @@ -7115,9 +7483,9 @@ source-map "^0.7.3" "@types/webpack@^4.4.31": - version "4.41.38" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.38.tgz#5a40ac81bdd052bf405e8bdcf3e1236f6db6dc26" - integrity sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw== + version "4.41.39" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.39.tgz#ab6feaeef8e074d0b584bbe4a4e2dc604b58eed7" + integrity sha512-otxUJvoi6FbBq/64gGH34eblpKLgdi+gf08GaAh8Bx6So0ZZic028Ev/SUxD22gbthMKCkeeiXEat1kHLDJfYg== dependencies: "@types/node" "*" "@types/tapable" "^1" @@ -7127,14 +7495,14 @@ source-map "^0.6.0" "@types/webxr@^0.5.5": - version "0.5.19" - resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.19.tgz#463a27bc06ff1c0a0c997e86b48bf24c5f50a4af" - integrity sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw== + version "0.5.20" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.20.tgz#b16b681af314ec011b2e8221b0a072d691c04953" + integrity sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg== "@types/ws@^8.2.2", "@types/ws@^8.5.5": - version "8.5.10" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== dependencies: "@types/node" "*" @@ -7151,9 +7519,9 @@ "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.32" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" - integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== dependencies: "@types/yargs-parser" "*" @@ -7528,9 +7896,9 @@ acorn-walk@^7.0.0, acorn-walk@^7.2.0: integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== acorn-walk@^8.0.0, acorn-walk@^8.0.2: - version "8.3.3" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" - integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" @@ -7544,11 +7912,16 @@ acorn@^7.0.0, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.0, acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.0.0, acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.12.1: + version "8.13.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -7560,9 +7933,9 @@ address@^1.0.1, address@^1.1.2: integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== adm-zip@^0.5.10: - version "0.5.14" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.14.tgz#2c557c0bf12af4311cf6d32970f4060cf8133b2a" - integrity sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg== + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== agent-base@5: version "5.1.1" @@ -7651,14 +8024,14 @@ ajv@^6.1.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" - integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.4.1" algoliasearch-helper@^3.13.3: version "3.22.5" @@ -7726,7 +8099,7 @@ ansi-escapes@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: +ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== @@ -7764,9 +8137,9 @@ ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^2.2.1: version "2.2.1" @@ -7862,20 +8235,13 @@ aria-hidden@^1.1.1: dependencies: tslib "^2.0.0" -aria-query@5.1.3: +aria-query@5.1.3, aria-query@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: deep-equal "^2.0.5" -aria-query@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" @@ -7904,7 +8270,7 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8: +array-includes@^3.1.6, array-includes@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== @@ -7961,7 +8327,7 @@ array.prototype.findlast@^1.2.5: es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" -array.prototype.findlastindex@^1.2.3: +array.prototype.findlastindex@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== @@ -8006,17 +8372,7 @@ array.prototype.reduce@^1.0.6: es-object-atoms "^1.0.0" is-string "^1.0.7" -array.prototype.toreversed@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" - integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.tosorted@^1.1.3: +array.prototype.tosorted@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== @@ -8109,9 +8465,9 @@ async@^2.6.4: lodash "^4.17.14" async@^3.2.0, async@^3.2.3: - version "3.2.5" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" - integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== asynckit@^0.4.0: version "0.4.0" @@ -8124,9 +8480,9 @@ at-least-node@^1.0.0: integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== attr-accept@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" - integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + version "2.2.4" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.4.tgz#e28749d5975732586aea03c8912e2d0f1d1d77e7" + integrity sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w== autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.4: version "10.4.19" @@ -8165,14 +8521,14 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.0.tgz#d9b802e9bb9c248d7be5f7f5ef178dc3684e9dcc" - integrity sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g== + version "1.13.2" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== -axe-core@=4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" - integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axe-core@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" + integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== axios@^0.27.2: version "0.27.2" @@ -8192,20 +8548,18 @@ axios@^0.28.0: proxy-from-env "^1.1.0" axios@^1.0.0: - version "1.7.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" - integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" - integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== - dependencies: - dequal "^2.0.3" +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== b4a@^1.6.4, b4a@^1.6.6: version "1.6.7" @@ -8272,7 +8626,7 @@ babel-loader@^9.0.0, babel-loader@^9.1.2: find-cache-dir "^4.0.0" schema-utils "^4.0.0" -babel-loader@^9.1.3: +babel-loader@^9.2.1: version "9.2.1" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== @@ -8342,15 +8696,7 @@ babel-plugin-polyfill-corejs2@^0.4.10, babel-plugin-polyfill-corejs2@^0.4.6: "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: - version "0.10.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" - integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.1" - core-js-compat "^3.36.1" - -babel-plugin-polyfill-corejs3@^0.10.6: +babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4, babel-plugin-polyfill-corejs3@^0.10.6: version "0.10.6" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== @@ -8381,22 +8727,25 @@ babel-plugin-polyfill-regenerator@^0.6.1: "@babel/helper-define-polyfill-provider" "^0.6.2" babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" babel-preset-jest@^29.6.3: version "29.6.3" @@ -8666,6 +9015,16 @@ browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^ node-releases "^2.0.18" update-browserslist-db "^1.1.0" +browserslist@^4.24.2: + version "4.24.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -8883,11 +9242,21 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599: +caniuse-lite@^1.0.0: version "1.0.30001636" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== +caniuse-lite@^1.0.30001599: + version "1.0.30001668" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz#98e214455329f54bf7a4d70b49c9794f0fbedbed" + integrity sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw== + +caniuse-lite@^1.0.30001616, caniuse-lite@^1.0.30001669: + version "1.0.30001675" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001675.tgz#0c1f01fc9cc543b61839753a4c234f995588d1b9" + integrity sha512-/wV1bQwPrkLiQMjaJF5yUMVM/VdRPOCU8QZ+PmG6uW6DvYSrNY1bpwHI/3mOcUosLaJCzYDi5o91IQB51ft6cg== + caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: version "1.0.30001667" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz#99fc5ea0d9c6e96897a104a8352604378377f949" @@ -9030,7 +9399,7 @@ child-process-promise@^2.2.1: node-version "^1.0.0" promise-polyfill "^6.0.1" -chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: +chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -9078,9 +9447,9 @@ citty@^0.1.6: consola "^3.2.3" cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.3: - version "1.3.1" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz#c485341ae8fd999ca4ee5af2d7a1c9ae01e0099c" - integrity sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q== + version "1.4.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" + integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== class-variance-authority@^0.7.0: version "0.7.0" @@ -9353,7 +9722,7 @@ combine-promises@^1.1.0: resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.2.0.tgz#5f2e68451862acf85761ded4d9e2af7769c2ca6a" integrity sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ== -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -9399,9 +9768,9 @@ compare-func@^2.0.0: dot-prop "^5.1.0" complex.js@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31" - integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg== + version "2.2.5" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.2.5.tgz#505b9a71a95a3e6dd5687814e04072b5c0fa573d" + integrity sha512-U3pSYTZz5Af/xvHgKQkJYHBMGmae7Ms51qqJougCR05YWF1Fihef4LRfOpBFONH2gvPFHMZq2rhx0I44DG23xw== compressible@~2.0.16: version "2.0.18" @@ -9448,6 +9817,11 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + config-chain@^1.1.11: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -9482,11 +9856,6 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - consola@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" @@ -9658,14 +10027,7 @@ copy-webpack-plugin@^9.0.1: schema-utils "^3.1.1" serialize-javascript "^6.0.0" -core-js-compat@^3.31.0, core-js-compat@^3.33.1, core-js-compat@^3.36.1: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" - integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== - dependencies: - browserslist "^4.23.0" - -core-js-compat@^3.38.0, core-js-compat@^3.38.1: +core-js-compat@^3.31.0, core-js-compat@^3.33.1, core-js-compat@^3.38.0, core-js-compat@^3.38.1: version "3.38.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== @@ -9673,9 +10035,9 @@ core-js-compat@^3.38.0, core-js-compat@^3.38.1: browserslist "^4.23.3" core-js-pure@^3.23.3, core-js-pure@^3.30.2: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" - integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.38.1.tgz#e8534062a54b7221344884ba9b52474be495ada3" + integrity sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ== core-js@*, core-js@^3.2.1, core-js@^3.8.3: version "3.37.1" @@ -10380,9 +10742,9 @@ dateformat@^3.0.3: integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== dayjs@^1.10.4: - version "1.11.11" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" - integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== dcmjs@*, dcmjs@>=0.33.0, dcmjs@^0.29.8: version "0.34.5" @@ -10415,11 +10777,11 @@ debug@2.6.9, debug@^2.6.0, debug@^2.6.9: ms "2.0.0" debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" debug@4.3.4: version "4.3.4" @@ -10681,7 +11043,7 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.0, dequal@^2.0.2, dequal@^2.0.3: +dequal@^2.0.0, dequal@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -10698,13 +11060,6 @@ detect-gpu@^4.0.16: dependencies: webgl-constants "^1.1.1" -detect-gpu@^5.0.22: - version "5.0.38" - resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.38.tgz#1c05ce728ea1229d16db15b865631609bf0d6952" - integrity sha512-36QeGHSXYcJ/RfrnPEScR8GDprbXFG4ZhXsfVNVHztZr38+fRxgHnJl3CjYXXjbeRUhu3ZZBJh6Lg0A9v0Qd8A== - dependencies: - webgl-constants "^1.1.1" - detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -10715,6 +11070,11 @@ detect-indent@^6.1.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-libc@^2.0.0, detect-libc@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" @@ -11149,6 +11509,11 @@ electron-to-chromium@^1.5.28: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.35.tgz#1d38d386186c72b1fa6e74c3a7de5f888b503100" integrity sha512-hOSRInrIDm0Brzp4IHW2F/VM+638qOL2CzE0DgpnGzKW27C95IqqeqgKz/hxHGnvPxvQGpHUGD5qRVC9EZY2+A== +electron-to-chromium@^1.5.41: + version "1.5.49" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz#9358f514ab6eeed809a8689f4b39ea5114ae729c" + integrity sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -11261,9 +11626,9 @@ envinfo@7.8.1: integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== envinfo@^7.7.3: - version "7.13.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" - integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== enzyme-shallow-equal@^1.0.0: version "1.0.7" @@ -11292,7 +11657,7 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" -es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: +es-abstract@^1.17.2, es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -11376,10 +11741,10 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.19: - version "1.0.19" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" - integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== +es-iterator-helpers@^1.0.19: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" + integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" @@ -11388,18 +11753,18 @@ es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.19: es-set-tostringtag "^2.0.3" function-bind "^1.1.2" get-intrinsic "^1.2.4" - globalthis "^1.0.3" + globalthis "^1.0.4" has-property-descriptors "^1.0.2" has-proto "^1.0.3" has-symbols "^1.0.3" internal-slot "^1.0.7" - iterator.prototype "^1.1.2" + iterator.prototype "^1.1.3" safe-array-concat "^1.1.2" es-module-lexer@^1.2.1, es-module-lexer@^1.4.1: - version "1.5.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" - integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== es-object-atoms@^1.0.0: version "1.0.0" @@ -11439,9 +11804,9 @@ esbuild-plugin-alias@^0.2.1: integrity sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ== esbuild-register@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.5.0.tgz#449613fb29ab94325c722f560f800dd946dc8ea8" - integrity sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A== + version "3.6.0" + resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.6.0.tgz#cf270cfa677baebbc0010ac024b823cbf723a36d" + integrity sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg== dependencies: debug "^4.3.4" @@ -11561,10 +11926,10 @@ eslint-loader@^2.0.0: object-hash "^1.1.4" rimraf "^2.6.1" -eslint-module-utils@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" - integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== dependencies: debug "^3.2.7" @@ -11592,49 +11957,51 @@ eslint-plugin-flowtype@^7.0.0: string-natural-compare "^3.0.1" eslint-plugin-import@^2.26.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" - integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== dependencies: - array-includes "^3.1.7" - array.prototype.findlastindex "^1.2.3" + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.8" + array.prototype.findlastindex "^1.2.5" array.prototype.flat "^1.3.2" array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.8.0" - hasown "^2.0.0" - is-core-module "^2.13.1" + eslint-module-utils "^2.12.0" + hasown "^2.0.2" + is-core-module "^2.15.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.fromentries "^2.0.7" - object.groupby "^1.0.1" - object.values "^1.1.7" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.0" semver "^6.3.1" + string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" - integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + version "6.10.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" + integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== dependencies: - "@babel/runtime" "^7.23.2" - aria-query "^5.3.0" - array-includes "^3.1.7" + aria-query "~5.1.3" + array-includes "^3.1.8" array.prototype.flatmap "^1.3.2" ast-types-flow "^0.0.8" - axe-core "=4.7.0" - axobject-query "^3.2.1" + axe-core "^4.10.0" + axobject-query "^4.1.0" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.15" - hasown "^2.0.0" + es-iterator-helpers "^1.0.19" + hasown "^2.0.2" jsx-ast-utils "^3.3.5" language-tags "^1.0.9" minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.0" eslint-plugin-node@^11.1.0: version "11.1.0" @@ -11649,12 +12016,12 @@ eslint-plugin-node@^11.1.0: semver "^6.1.0" eslint-plugin-prettier@^5.0.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" - integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== + version "5.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95" + integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw== dependencies: prettier-linter-helpers "^1.0.0" - synckit "^0.8.6" + synckit "^0.9.1" eslint-plugin-promise@^5.2.0: version "5.2.0" @@ -11667,28 +12034,28 @@ eslint-plugin-react-hooks@^4.4.0: integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.29.4: - version "7.34.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz#2780a1a35a51aca379d86d29b9a72adc6bfe6b66" - integrity sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw== + version "7.37.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" + integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" - array.prototype.toreversed "^1.1.2" - array.prototype.tosorted "^1.1.3" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.8" object.fromentries "^2.0.8" - object.hasown "^1.1.4" object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-plugin-tsdoc@^0.2.11: version "0.2.17" @@ -11752,15 +12119,15 @@ eslint-webpack-plugin@^2.5.3: schema-utils "^3.1.1" eslint@^8.39.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -11810,9 +12177,9 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -12213,6 +12580,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" + integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== + fast-url-parser@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -12380,12 +12752,11 @@ finalhandler@1.3.1: unpipe "~1.0.0" find-babel-config@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-2.1.1.tgz#93703fc8e068db5e4c57592900c5715dd04b7e5b" - integrity sha512-5Ji+EAysHGe1OipH7GN4qDjok5Z1uw5KAwDCbicU/4wyTZY7CqOCzcWbG7J5ad9mazq67k89fXlbc1MuIfl9uA== + version "2.1.2" + resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-2.1.2.tgz#2841b1bfbbbcdb971e1e39df8cbc43dafa901716" + integrity sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg== dependencies: json5 "^2.2.3" - path-exists "^4.0.0" find-cache-dir@^0.1.1: version "0.1.1" @@ -12493,9 +12864,9 @@ flatted@^3.2.9: integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== flow-parser@0.*: - version "0.238.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.238.0.tgz#b465753c2630a38f459413a745c69ec11a0b5291" - integrity sha512-VE7XSv1epljsIN2YeBnxCmGJihpNIAnLLu/pPOdA+Gkso7qDltJwUi6vfHjgxdBbjSdAuPGnhuOHJUQG+yYwIg== + version "0.247.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.247.1.tgz#4a512c882cd08126825fe00b1ee452637ef44371" + integrity sha512-DHwcm06fWbn2Z6uFD3NaBZ5lMOoABIQ4asrVA80IWvYjjT5WdbghkUOL1wIcbLcagnFTdCZYOlSNnKNp/xnRZQ== follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.6: version "1.15.6" @@ -12510,9 +12881,9 @@ for-each@^0.3.3: is-callable "^1.1.3" foreground-child@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" - integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -12564,7 +12935,7 @@ form-data-encoder@^2.1.2: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== -form-data@^4.0.0: +form-data@^4.0.0, form-data@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== @@ -12573,15 +12944,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - format@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" @@ -12720,7 +13082,7 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.2, function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: +function.prototype.name@^1.1.2, function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== @@ -12981,18 +13343,7 @@ glob@7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^10.0.0, glob@^10.2.2: - version "10.4.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" - integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - path-scurry "^1.11.1" - -glob@^10.3.10: +glob@^10.0.0, glob@^10.2.2, glob@^10.3.10: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -13087,7 +13438,7 @@ globalthis@1.0.3: dependencies: define-properties "^1.1.3" -globalthis@^1.0.3: +globalthis@^1.0.3, globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -13564,7 +13915,7 @@ html-void-elements@^3.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -html-webpack-plugin@^5.3.2, html-webpack-plugin@^5.5.0, html-webpack-plugin@^5.5.3: +html-webpack-plugin@^5.3.2, html-webpack-plugin@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== @@ -13575,6 +13926,17 @@ html-webpack-plugin@^5.3.2, html-webpack-plugin@^5.5.0, html-webpack-plugin@^5.5 pretty-error "^4.0.0" tapable "^2.0.0" +html-webpack-plugin@^5.6.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + html2canvas@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -13593,7 +13955,7 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" -htmlparser2@^8.0.1: +htmlparser2@^8.0.0, htmlparser2@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== @@ -13668,14 +14030,14 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-signature@~1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" - integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== +http-signature@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f" + integrity sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg== dependencies: assert-plus "^1.0.0" jsprim "^2.0.2" - sshpk "^1.14.1" + sshpk "^1.18.0" http2-wrapper@^2.1.10: version "2.2.1" @@ -13810,9 +14172,9 @@ ignore-walk@^6.0.0: minimatch "^9.0.0" ignore@^5.0.0, ignore@^5.0.4, ignore@^5.1.1, ignore@^5.1.9, ignore@^5.2.0, ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== image-size@^1.0.2: version "1.1.1" @@ -13854,7 +14216,7 @@ import-lazy@^4.0.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== -import-local@3.1.0, import-local@^3.0.2: +import-local@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== @@ -13862,6 +14224,14 @@ import-local@3.1.0, import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -13882,10 +14252,10 @@ infer-owner@^1.0.4: resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== -infima@0.2.0-alpha.44: - version "0.2.0-alpha.44" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.44.tgz#9cd9446e473b44d49763f48efabe31f32440861d" - integrity sha512-tuRkUSO/lB3rEhLJk25atwAjgLuzq070+pOW8XcvpHky/YbENnRRdPd85IBkyeTgttmOy5ah+yHYsK1HhUd4lQ== +infima@0.2.0-alpha.45: + version "0.2.0-alpha.45" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.45.tgz#542aab5a249274d81679631b492973dd2c1e7466" + integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== inflight@^1.0.4: version "1.0.6" @@ -14003,9 +14373,8 @@ ip-address@^9.0.5: jsbn "1.1.0" sprintf-js "^1.1.3" -ip@^1.1.0, ip@^2.0.1, "ip@https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz": +ip@^1.1.0, "ip@https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz": version "9.0.5" - uid "117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" dependencies: jsbn "1.1.0" @@ -14116,7 +14485,7 @@ is-ci@3.0.1, is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.1.0, is-core-module@^2.13.1, is-core-module@^2.5.0, is-core-module@^2.8.1: +is-core-module@^2.1.0, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== @@ -14130,6 +14499,13 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.2" +is-core-module@^2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + is-data-view@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" @@ -14559,9 +14935,9 @@ istanbul-lib-instrument@^5.0.4: semver "^6.3.0" istanbul-lib-instrument@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz#91655936cf7380e4e473383081e38478b69993b1" - integrity sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw== + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== dependencies: "@babel/core" "^7.23.9" "@babel/parser" "^7.23.9" @@ -14595,10 +14971,10 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterator.prototype@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" - integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== +iterator.prototype@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" + integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== dependencies: define-properties "^1.2.1" get-intrinsic "^1.2.1" @@ -14607,18 +14983,18 @@ iterator.prototype@^1.1.2: set-function-name "^2.0.1" jackspeak@^3.1.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" - integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" jake@^10.8.5: - version "10.9.1" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.1.tgz#8dc96b7fcc41cb19aa502af506da4e1d56f5e62b" - integrity sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== dependencies: async "^3.2.3" chalk "^4.0.2" @@ -15208,11 +15584,6 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - jsesc@^3.0.2, jsesc@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" @@ -15389,9 +15760,9 @@ latest-version@^7.0.0: package-json "^8.1.0" launch-editor@^2.6.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.7.0.tgz#53ba12b3eb131edefee99acaef7850c40272273f" - integrity sha512-KAc66u6LxWL8MifQ94oG3YGKYWDwz/Gi0T15lN//GaQoZe08vQGFJxrXkPAeu50UXgvJPPaRKVGuP1TRUm/aHQ== + version "2.9.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.9.1.tgz#253f173bd441e342d4344b4dae58291abb425047" + integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== dependencies: picocolors "^1.0.0" shell-quote "^1.8.1" @@ -15536,6 +15907,74 @@ libnpmpublish@7.3.0: sigstore "^1.4.0" ssri "^10.0.1" +lightningcss-darwin-arm64@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz#565bd610533941cba648a70e105987578d82f996" + integrity sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ== + +lightningcss-darwin-x64@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz#c906a267237b1c7fe08bff6c5ac032c099bc9482" + integrity sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg== + +lightningcss-freebsd-x64@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz#a7c3c4d6ee18dffeb8fa69f14f8f9267f7dc0c34" + integrity sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA== + +lightningcss-linux-arm-gnueabihf@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz#c7c16432a571ec877bf734fe500e4a43d48c2814" + integrity sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA== + +lightningcss-linux-arm64-gnu@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz#cfd9e18df1cd65131da286ddacfa3aee6862a752" + integrity sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A== + +lightningcss-linux-arm64-musl@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz#6682ff6b9165acef9a6796bd9127a8e1247bb0ed" + integrity sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg== + +lightningcss-linux-x64-gnu@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz#714221212ad184ddfe974bbb7dbe9300dfde4bc0" + integrity sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A== + +lightningcss-linux-x64-musl@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz#247958daf622a030a6dc2285afa16b7184bdf21e" + integrity sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA== + +lightningcss-win32-arm64-msvc@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz#64cfe473c264ef5dc275a4d57a516d77fcac6bc9" + integrity sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ== + +lightningcss-win32-x64-msvc@1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz#237d0dc87d9cdc9cf82536bcbc07426fa9f3f422" + integrity sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw== + +lightningcss@^1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.27.0.tgz#d4608e63044343836dd9769f6c8b5d607867649a" + integrity sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ== + dependencies: + detect-libc "^1.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.27.0" + lightningcss-darwin-x64 "1.27.0" + lightningcss-freebsd-x64 "1.27.0" + lightningcss-linux-arm-gnueabihf "1.27.0" + lightningcss-linux-arm64-gnu "1.27.0" + lightningcss-linux-arm64-musl "1.27.0" + lightningcss-linux-x64-gnu "1.27.0" + lightningcss-linux-x64-musl "1.27.0" + lightningcss-win32-arm64-msvc "1.27.0" + lightningcss-win32-x64-msvc "1.27.0" + lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -15744,7 +16183,7 @@ lodash-es@^4.17.15: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash.clonedeep@4.5.0, lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== @@ -15875,9 +16314,9 @@ lowercase-keys@^3.0.0: integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== lru-cache@^10.2.0: - version "10.2.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" - integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^4.0.1: version "4.1.5" @@ -15924,11 +16363,11 @@ magic-string@^0.25.0, magic-string@^0.25.2, magic-string@^0.25.7: sourcemap-codec "^1.4.8" magic-string@^0.30.5: - version "0.30.10" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" - integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + version "0.30.11" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" + integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/sourcemap-codec" "^1.5.0" make-dir@4.0.0, make-dir@^4.0.0: version "4.0.0" @@ -16023,24 +16462,31 @@ map-stream@~0.1.0: integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== mapbox-to-css-font@^2.4.1: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.4.tgz#f4d9b8ad95a8c540fccfef23bb9e341a5e4f4883" - integrity sha512-X1dtuTuH2D1MRMuductMZCLV/fy9EoIgqW/lmu8vQSAhEatx/tdFebkYT3TVhdTwqFDHbLEgQBD3IKA4KI7aoQ== + version "2.4.5" + resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.5.tgz#b10a7a33af3e1a9a1369e4d5e8285492a7943c46" + integrity sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w== markdown-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + markdown-table@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== markdown-to-jsx@^7.1.8: - version "7.4.7" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz#740ee7ec933865ef5cc683a0992797685a75e2ee" - integrity sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg== + version "7.5.0" + resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.5.0.tgz#42ece0c71e842560a7d8bd9f81e7a34515c72150" + integrity sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw== material-colors@^1.2.1: version "1.2.6" @@ -17184,11 +17630,16 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -17256,10 +17707,10 @@ mini-css-extract-plugin@^2.1.0: schema-utils "^4.0.0" tapable "^2.2.1" -mini-css-extract-plugin@^2.7.6: - version "2.9.1" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz#4d184f12ce90582e983ccef0f6f9db637b4be758" - integrity sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ== +mini-css-extract-plugin@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" + integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" @@ -17305,9 +17756,9 @@ minimatch@^8.0.2: brace-expansion "^2.0.1" minimatch@^9.0.0, minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -17362,9 +17813,9 @@ minipass-flush@^1.0.5: minipass "^3.0.0" minipass-json-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" - integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz#5121616c77a11c406c3ffa77509e0b77bb267ec3" + integrity sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg== dependencies: jsonparse "^1.3.1" minipass "^3.0.0" @@ -17430,6 +17881,16 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mlly@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.2.tgz#21c0d04543207495b8d867eff0ac29fac9a023c0" + integrity sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA== + dependencies: + acorn "^8.12.1" + pathe "^1.1.2" + pkg-types "^1.2.0" + ufo "^1.5.4" + modify-values@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -17472,7 +17933,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -17590,9 +18051,9 @@ no-case@^3.0.4: tslib "^2.0.3" node-abi@^3.3.0: - version "3.65.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.65.0.tgz#ca92d559388e1e9cab1680a18c1a18757cdac9d3" - integrity sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA== + version "3.68.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.68.0.tgz#8f37fb02ecf4f43ebe694090dcb52e0c4cc4ba25" + integrity sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A== dependencies: semver "^7.3.5" @@ -17667,9 +18128,9 @@ node-forge@^1: integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-gyp-build@^4.3.0: - version "4.8.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" - integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== + version "4.8.2" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa" + integrity sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw== node-gyp@^9.0.0: version "9.4.1" @@ -17893,10 +18354,10 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" -npm-to-yarn@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/npm-to-yarn/-/npm-to-yarn-2.2.1.tgz#048843a6630621daffc6a239dfc89698b8abf7e8" - integrity sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ== +npm-to-yarn@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/npm-to-yarn/-/npm-to-yarn-3.0.0.tgz#05006d97359e285f0316e249dbbe56f377ca1182" + integrity sha512-76YnmsbfrYp0tMsWxM0RNX0Vs+x8JxpJGu6B/jDn4lW8+laiTcKmKi9MeMh4UikO4RkJ1oqURoDy9bXJmMXS6A== npmlog@^6.0.0, npmlog@^6.0.2: version "6.0.2" @@ -17920,15 +18381,23 @@ nth-check@^1.0.2, nth-check@^2.0.1, nth-check@^2.1.1: dependencies: boolbase "^1.0.0" +null-loader@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a" + integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== nwsapi@^2.2.2: - version "2.2.10" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8" - integrity sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ== + version "2.2.13" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" + integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== nx@16.10.0, "nx@>=16.5.1 < 17": version "16.10.0" @@ -17984,15 +18453,16 @@ nx@16.10.0, "nx@>=16.5.1 < 17": "@nx/nx-win32-x64-msvc" "16.10.0" nypm@^0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.8.tgz#a16b078b161be5885351e72cf0b97326973722bf" - integrity sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og== + version "0.3.12" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.12.tgz#37541bec0af3a37d3acd81d6662c6666e650b22e" + integrity sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA== dependencies: citty "^0.1.6" consola "^3.2.3" execa "^8.0.1" pathe "^1.1.2" - ufo "^1.4.0" + pkg-types "^1.2.0" + ufo "^1.5.4" object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" @@ -18015,9 +18485,9 @@ object-hash@^3.0.0: integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== object-inspect@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== object-is@^1.1.2, object-is@^1.1.5: version "1.1.6" @@ -18042,7 +18512,7 @@ object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.4, object.assign@ has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.2, object.entries@^1.1.7, object.entries@^1.1.8: +object.entries@^1.1.2, object.entries@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== @@ -18051,7 +18521,7 @@ object.entries@^1.1.2, object.entries@^1.1.7, object.entries@^1.1.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -object.fromentries@^2.0.7, object.fromentries@^2.0.8: +object.fromentries@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== @@ -18074,7 +18544,7 @@ object.getownpropertydescriptors@^2.1.0: gopd "^1.0.1" safe-array-concat "^1.1.2" -object.groupby@^1.0.1: +object.groupby@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== @@ -18083,16 +18553,7 @@ object.groupby@^1.0.1: define-properties "^1.2.1" es-abstract "^1.23.2" -object.hasown@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" - integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== - dependencies: - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.values@^1.1.0, object.values@^1.1.5, object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: +object.values@^1.1.0, object.values@^1.1.5, object.values@^1.1.6, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== @@ -18112,14 +18573,14 @@ obuf@^1.0.0, obuf@^1.1.2: integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== ohash@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" - integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== + version "1.1.4" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" + integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== oidc-client-ts@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7" - integrity sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg== + version "3.1.0" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz#28d3254951a1c64cc9780042c61492a71b2240dd" + integrity sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g== dependencies: jwt-decode "^4.0.0" @@ -18529,6 +18990,11 @@ parse-path@^7.0.0: dependencies: protocols "^2.0.0" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== + parse-url@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d" @@ -18823,6 +19289,15 @@ pkg-install@^1.0.0: "@types/node" "^11.9.4" execa "^1.0.0" +pkg-types@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.1.tgz#6ac4e455a5bb4b9a6185c1c79abd544c901db2e5" + integrity sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw== + dependencies: + confbox "^0.1.8" + mlly "^1.7.2" + pathe "^1.1.2" + pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -19606,15 +20081,7 @@ postcss-selector-not@^6.0.1: dependencies: postcss-selector-parser "^6.0.10" -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" - integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.1.1: +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9, postcss-selector-parser@^6.1.1: version "6.1.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== @@ -19678,7 +20145,7 @@ postcss@^8.2.1, postcss@^8.3.5, postcss@^8.4.18, postcss@^8.4.23, postcss@^8.4.3 picocolors "^1.0.0" source-map-js "^1.2.0" -postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38, postcss@^8.4.47: +postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38, postcss@^8.4.47: version "8.4.47" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== @@ -19722,25 +20189,25 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier-plugin-tailwindcss@0.6.8: + version "0.6.8" + resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz#8a178e1679e3f941cc9de396f109c6cffea676d8" + integrity sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA== + prettier-plugin-tailwindcss@^0.5.4: version "0.5.14" resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.14.tgz#4482eed357d5e22eac259541c70aca5a4c7b9d5c" integrity sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q== -prettier-plugin-tailwindcss@^0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz#e05202784a3f41889711ae38c75c5b8cad72f368" - integrity sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ== - prettier@^2.8.0: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -prettier@^3.0.3, prettier@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" - integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== +prettier@^3.0.3, prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1, pretty-bytes@^5.6.0: version "5.6.0" @@ -19860,10 +20327,11 @@ promzard@^1.0.0: read "^3.0.1" prop-types-exact@^1.2.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.4.tgz#9010e6b844a0782f6636a597e1738ed494bf7943" - integrity sha512-vKfETKgBHRCLQwZgpl0pGPvMFxCX/06dAkz5jwNYHfrU0I8bgVhryaHA6O/KlqwtQi0IdnjAhDiZqzD+uJuVjA== + version "1.2.5" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.5.tgz#f275e7dc0d629c2f7414782e8189b3e2d2e9e158" + integrity sha512-wHDhA5TSSvU07gdzsdeT/FZg6zay94K4Y7swSK4YsRG3moWB0Qsp9g1Y5BBausP1HF8K4UeVe2Xt7ZFJByKp6A== dependencies: + call-bind "^1.0.7" es-errors "^1.3.0" hasown "^2.0.2" isarray "^2.0.5" @@ -19943,9 +20411,9 @@ pump@^2.0.0: once "^1.3.1" pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== dependencies: end-of-stream "^1.1.0" once "^1.3.1" @@ -20002,27 +20470,13 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@6.10.4: - version "6.10.4" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7" - integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== - dependencies: - side-channel "^1.0.4" - -qs@6.13.0: +qs@6.13.0, qs@^6.10.0, qs@^6.12.3: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" -qs@^6.10.0, qs@^6.11.2: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== - dependencies: - side-channel "^1.0.6" - query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -20432,7 +20886,7 @@ react-refresh@^0.14.0, react-refresh@^0.14.2: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== -react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4: +react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== @@ -20451,12 +20905,12 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-remove-scroll@2.5.7: - version "2.5.7" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb" - integrity sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA== +react-remove-scroll@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" + integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== dependencies: - react-remove-scroll-bar "^2.3.4" + react-remove-scroll-bar "^2.3.6" react-style-singleton "^2.2.1" tslib "^2.1.0" use-callback-ref "^1.3.0" @@ -20490,12 +20944,12 @@ react-router-dom@^5.3.4: tiny-warning "^1.0.0" react-router-dom@^6.8.1: - version "6.23.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.23.1.tgz#30cbf266669693e9492aa4fc0dde2541ab02322f" - integrity sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ== + version "6.26.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.26.2.tgz#a6e3b0cbd6bfd508e42b9342099d015a0ac59680" + integrity sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ== dependencies: - "@remix-run/router" "1.16.1" - react-router "6.23.1" + "@remix-run/router" "1.19.2" + react-router "6.26.2" react-router@5.3.4, react-router@^5.3.4: version "5.3.4" @@ -20512,12 +20966,12 @@ react-router@5.3.4, react-router@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@6.23.1, react-router@^6.23.1: - version "6.23.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.1.tgz#d08cbdbd9d6aedc13eea6e94bc6d9b29cb1c4be9" - integrity sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ== +react-router@6.26.2, react-router@^6.23.1: + version "6.26.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.26.2.tgz#2f0a68999168954431cdc29dd36cec3b6fa44a7e" + integrity sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A== dependencies: - "@remix-run/router" "1.16.1" + "@remix-run/router" "1.19.2" react-select@5.7.4: version "5.7.4" @@ -20866,14 +21320,14 @@ regenerator-transform@^0.15.2: "@babel/runtime" "^7.8.4" regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== dependencies: - call-bind "^1.0.6" + call-bind "^1.0.7" define-properties "^1.2.1" es-errors "^1.3.0" - set-function-name "^2.0.1" + set-function-name "^2.0.2" regexpp@^3.0.0: version "3.2.0" @@ -21112,6 +21566,11 @@ renderkid@^3.0.0: lodash "^4.17.21" strip-ansi "^6.0.1" +repeat-string@^1.0.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" @@ -21401,6 +21860,18 @@ safe-regex-test@^1.0.3: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-html@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.1.tgz#b4639b0a09574ab62b1b353cb99b1b87af742834" + integrity sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^8.0.0" + is-plain-object "^5.0.0" + parse-srcset "^1.0.2" + postcss "^8.3.11" + sax@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" @@ -21527,9 +21998,9 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.0, semver@^6.3.1: integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.3, semver@^7.5.4: - version "7.6.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" - integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== send@0.19.0: version "0.19.0" @@ -21564,7 +22035,7 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -serve-handler@6.1.5, serve-handler@^6.1.5: +serve-handler@6.1.5: version "6.1.5" resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== @@ -21578,6 +22049,19 @@ serve-handler@6.1.5, serve-handler@^6.1.5: path-to-regexp "2.2.1" range-parser "1.2.0" +serve-handler@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" + integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + mime-types "2.1.18" + minimatch "3.1.2" + path-is-inside "1.0.2" + path-to-regexp "3.3.0" + range-parser "1.2.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -22048,9 +22532,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.18" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz#22aa922dcf2f2885a6494a261f2d8b75345d0326" - integrity sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ== + version "3.0.20" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" + integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== spdx-license-list@^6.4.0: version "6.9.0" @@ -22121,7 +22605,7 @@ srcset@^4.0.0: resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4" integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== -sshpk@^1.14.1: +sshpk@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== @@ -22196,7 +22680,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.0.1: +std-env@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== @@ -22214,11 +22698,11 @@ store2@^2.14.2: integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg== storybook@^7.6.10: - version "7.6.19" - resolved "https://registry.yarnpkg.com/storybook/-/storybook-7.6.19.tgz#3edb81cfd26d8f710e562419f38bc39ff25da84c" - integrity sha512-xWD1C4vD/4KMffCrBBrUpsLUO/9uNpm8BVW8+Vcb30gkQDfficZ0oziWkmLexpT53VSioa24iazGXMwBqllYjQ== + version "7.6.20" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-7.6.20.tgz#6204ff0c28471536a1a64cb16d1c97872dd33f95" + integrity sha512-Wt04pPTO71pwmRmsgkyZhNo4Bvdb/1pBAMsIFb9nQLykEdzzpXjvingxFFvdOG4nIowzwgxD+CLlyRqVJqnATw== dependencies: - "@storybook/cli" "7.6.19" + "@storybook/cli" "7.6.20" stream-browserify@3.0.0: version "3.0.0" @@ -22323,6 +22807,14 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.prototype.includes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" + integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" @@ -22341,6 +22833,14 @@ string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6: set-function-name "^2.0.2" side-channel "^1.0.6" +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -22672,7 +23172,7 @@ svgo@^3.0.2, svgo@^3.2.0: csso "^5.0.5" picocolors "^1.0.0" -swc-loader@^0.2.3: +swc-loader@^0.2.3, swc-loader@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== @@ -22702,20 +23202,18 @@ synchronous-promise@^2.0.15: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032" integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g== -synckit@^0.8.6: - version "0.8.8" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" - integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== +synckit@^0.9.1: + version "0.9.2" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" + integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== dependencies: "@pkgr/core" "^0.1.0" tslib "^2.6.2" tailwind-merge@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.3.0.tgz#27d2134fd00a1f77eca22bcaafdd67055917d286" - integrity sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA== - dependencies: - "@babel/runtime" "^7.24.1" + version "2.5.3" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.3.tgz#579546e14ddda24462e0303acd8798c50f5511bb" + integrity sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw== tailwindcss-animate@^1.0.7: version "1.0.7" @@ -23050,9 +23548,9 @@ to-regex-range@^5.0.1: is-number "^7.0.0" tocbot@^4.20.1: - version "4.28.2" - resolved "https://registry.yarnpkg.com/tocbot/-/tocbot-4.28.2.tgz#5a51b34cefd39f6b556b936b380a838a0a8c49ea" - integrity sha512-/MaSa9xI6mIo84IxqqliSCtPlH0oy7sLcY9s26qPMyH/2CxtZ2vNAXYlIdEQ7kjAkCQnc0rbLygf//F5c663oQ== + version "4.30.0" + resolved "https://registry.yarnpkg.com/tocbot/-/tocbot-4.30.0.tgz#0730d02b41284cc25ba4fbc0f1a38858464df21a" + integrity sha512-c0ucneDHP5zqsbczYxjRj1SWD+6EeD1mUbQPTdVJzNe0aAZH4NyeZt4cQQBjWU4v8tTlJtzDzEgD03sCc5nL1w== toidentifier@1.0.1: version "1.0.1" @@ -23309,20 +23807,20 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@4.6.4, "typescript@>=3 < 6", typescript@~5.2.2: - version "4.6.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" - integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== +typescript@5.5.4, "typescript@>=3 < 6", typescript@~5.2.2: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== -ufo@^1.4.0: - version "1.5.3" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" - integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== +ufo@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== uglify-js@^3.1.4: - version "3.18.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.18.0.tgz#73b576a7e8fda63d2831e293aeead73e0a270deb" - integrity sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A== + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== unbox-primitive@^1.0.2: version "1.0.2" @@ -23339,6 +23837,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -23358,9 +23861,9 @@ unicode-match-property-ecmascript@^2.0.0: unicode-property-aliases-ecmascript "^2.0.0" unicode-match-property-value-ecmascript@^2.0.0, unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" @@ -23559,14 +24062,12 @@ unpipe@1.0.0, unpipe@~1.0.0: integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== unplugin@^1.3.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.10.1.tgz#8ceda065dc71bc67d923dea0920f05c67f2cd68c" - integrity sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg== + version "1.14.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.14.1.tgz#c76d6155a661e43e6a897bce6b767a1ecc344c1a" + integrity sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w== dependencies: - acorn "^8.11.3" - chokidar "^3.6.0" - webpack-sources "^3.2.3" - webpack-virtual-modules "^0.6.1" + acorn "^8.12.1" + webpack-virtual-modules "^0.6.2" unquote@~1.1.1: version "1.1.1" @@ -23596,7 +24097,7 @@ upath@^1.2.0: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.1.0: +update-browserslist-db@^1.1.0, update-browserslist-db@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== @@ -23632,7 +24133,7 @@ update-notifier@^6.0.2: semver-diff "^4.0.0" xdg-basedir "^5.1.0" -uri-js@^4.2.2, uri-js@^4.4.1: +uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -23657,12 +24158,12 @@ url-parse@^1.5.3: requires-port "^1.0.0" url@^0.11.0: - version "0.11.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" - integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== + version "0.11.4" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" + integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== dependencies: punycode "^1.4.1" - qs "^6.11.2" + qs "^6.12.3" use-callback-ref@^1.3.0: version "1.3.2" @@ -23696,7 +24197,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.0.0: +use-sync-external-store@1.2.2, use-sync-external-store@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== @@ -23775,9 +24276,9 @@ v8-compile-cache@2.3.0: integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== v8-to-istanbul@^9.0.1: - version "9.2.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" - integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" @@ -23978,7 +24479,7 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== -webpack-bundle-analyzer@^4.8.0, webpack-bundle-analyzer@^4.9.0: +webpack-bundle-analyzer@^4.10.2, webpack-bundle-analyzer@^4.8.0: version "4.10.2" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== @@ -24071,7 +24572,7 @@ webpack-dev-server@4.7.3: webpack-dev-middleware "^5.3.0" ws "^8.1.0" -webpack-dev-server@^4.15.1: +webpack-dev-server@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== @@ -24125,6 +24626,15 @@ webpack-merge@*, webpack-merge@^5.7.3, webpack-merge@^5.9.0: flat "^5.0.2" wildcard "^2.0.0" +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + webpack-sources@^1.1.0, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" @@ -24143,12 +24653,12 @@ webpack-virtual-modules@^0.5.0: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== -webpack-virtual-modules@^0.6.1: +webpack-virtual-modules@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== -webpack@5, webpack@^5.88.1: +webpack@5, webpack@^5.88.1, webpack@^5.95.0: version "5.95.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== @@ -24206,15 +24716,19 @@ webpack@5.94.0: watchpack "^2.4.1" webpack-sources "^3.2.3" -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== +webpackbar@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b" + integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q== dependencies: - chalk "^4.1.0" - consola "^2.15.3" + ansi-escapes "^4.3.2" + chalk "^4.1.2" + consola "^3.2.3" + figures "^3.2.0" + markdown-table "^2.0.0" pretty-time "^1.1.0" - std-env "^3.0.1" + std-env "^3.7.0" + wrap-ansi "^7.0.0" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -24284,12 +24798,12 @@ which-boxed-primitive@^1.0.2: is-symbol "^1.0.3" which-builtin-type@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" - integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== dependencies: - function.prototype.name "^1.1.5" - has-tostringtag "^1.0.0" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" is-async-function "^2.0.0" is-date-object "^1.0.5" is-finalizationregistry "^1.0.2" @@ -24298,10 +24812,10 @@ which-builtin-type@^1.1.3: is-weakref "^1.0.2" isarray "^2.0.5" which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.9" + which-collection "^1.0.2" + which-typed-array "^1.1.15" -which-collection@^1.0.1: +which-collection@^1.0.1, which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== @@ -24311,7 +24825,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2, which-typed-array@^1.1.9: +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== @@ -24357,7 +24871,7 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" -wildcard@^2.0.0: +wildcard@^2.0.0, wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== @@ -24825,9 +25339,9 @@ ws@^7.3.1: integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.1.0, ws@^8.11.0, ws@^8.13.0, ws@^8.2.3: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: version "5.1.0" @@ -24975,15 +25489,22 @@ yocto-queue@^0.1.0: integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== zstddec@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.1.0.tgz#7050f3f0e0c3978562d0c566b3e5a427d2bad7ec" integrity sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg== +zustand@4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" + integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== + dependencies: + use-sync-external-store "1.2.2" + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"