fix(viewport): Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings (#4037)
This commit is contained in:
1 parent
a6c6fff43a
commit
f99a0bfb31
80 files changed
+1129
-801
No files matched your search
@@ -54,7 +54,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.6",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.8.0-beta.80",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.80",
|
||||
|
||||
@@ -12,7 +12,7 @@ function ViewerViewportGrid(props) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const { layout, activeViewportId, viewports } = viewportGrid;
|
||||
const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid;
|
||||
const { numCols, numRows } = layout;
|
||||
const elementRef = useRef(null);
|
||||
const layoutHash = useRef(null);
|
||||
@@ -97,11 +97,21 @@ function ViewerViewportGrid(props) {
|
||||
layoutType,
|
||||
layoutOptions,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout: true,
|
||||
});
|
||||
};
|
||||
|
||||
const _getUpdatedViewports = useCallback(
|
||||
(viewportId, displaySetInstanceUID) => {
|
||||
if (!isHangingProtocolLayout) {
|
||||
return [
|
||||
{
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: [displaySetInstanceUID],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
let updatedViewports = [];
|
||||
try {
|
||||
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
@@ -121,7 +131,7 @@ function ViewerViewportGrid(props) {
|
||||
|
||||
return updatedViewports;
|
||||
},
|
||||
[hangingProtocolService, uiNotificationService]
|
||||
[hangingProtocolService, uiNotificationService, isHangingProtocolLayout]
|
||||
);
|
||||
|
||||
// Using Hanging protocol engine to match the displaySets
|
||||
@@ -340,6 +350,7 @@ function ViewerViewportGrid(props) {
|
||||
viewportOptions={viewportOptions}
|
||||
displaySetOptions={displaySetOptions}
|
||||
needsRerendering={displaySetsNeedsRerendering}
|
||||
isHangingProtocolLayout={isHangingProtocolLayout}
|
||||
onElementEnabled={() => {
|
||||
viewportGridService.setViewportIsReady(viewportId, true);
|
||||
}}
|
||||
|
||||
@@ -18,7 +18,9 @@ function getPathQuestions(packageType) {
|
||||
{
|
||||
type: 'input',
|
||||
name: 'baseDir',
|
||||
message: `What is the target path to create your ${packageType} (we recommend you do not use the OHIF ${packageType} folder (./${packageType}s) unless you are developing a core ${packageType}):`,
|
||||
message: `What is the target path to create your ${packageType}?`,
|
||||
suffix: `\n(we recommend you do not use the OHIF ${packageType} folder (./${packageType}s) unless you are developing a core ${packageType})`,
|
||||
maxLength: 40,
|
||||
validate: input => {
|
||||
if (!input) {
|
||||
console.log('Please provide a valid target directory path');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { initToolGroups, moreTools, toolbarButtons } from '@ohif/mode-longitudinal';
|
||||
import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal';
|
||||
import { id } from './id';
|
||||
|
||||
const ohif = {
|
||||
@@ -64,7 +64,6 @@ function modeFactory({ modeConfiguration }) {
|
||||
const {
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
toolbarService,
|
||||
segmentationService,
|
||||
cornerstoneViewportService,
|
||||
uiDialogService,
|
||||
@@ -87,7 +86,9 @@ function modeFactory({ modeConfiguration }) {
|
||||
* A boolean return value that indicates whether the mode is valid for the
|
||||
* modalities of the selected studies. For instance a PET/CT mode should be
|
||||
*/
|
||||
isValidMode: ({ modalities }) => true,
|
||||
isValidMode: ({ modalities }) => {
|
||||
return { valid: true };
|
||||
},
|
||||
/**
|
||||
* Mode Routes are used to define the mode's behavior. A list of Mode Route
|
||||
* that includes the mode's path and the layout to be used. The layout will
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.6",
|
||||
"@ohif/ui": "3.8.0-beta.80",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@@ -14,6 +14,7 @@ class CineService extends PubSubService {
|
||||
};
|
||||
|
||||
serviceImplementation = {};
|
||||
startedClips = new Map();
|
||||
|
||||
constructor() {
|
||||
super(CineService.EVENTS);
|
||||
@@ -41,21 +42,26 @@ class CineService extends PubSubService {
|
||||
public playClip(element, playClipOptions) {
|
||||
const res = this.serviceImplementation._playClip(element, playClipOptions);
|
||||
|
||||
this.startedClips.set(element, playClipOptions);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: true });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public stopClip(element) {
|
||||
const res = this.serviceImplementation._stopClip(element);
|
||||
public stopClip(element, stopClipOptions) {
|
||||
const res = this.serviceImplementation._stopClip(element, stopClipOptions);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: false });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public _onModeExit() {
|
||||
public onModeExit() {
|
||||
this.setIsCineEnabled(false);
|
||||
this.startedClips.forEach((value, key) => {
|
||||
this.stopClip(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
public getSyncedViewports(viewportId) {
|
||||
|
||||
@@ -1388,6 +1388,22 @@ export default class HangingProtocolService extends PubSubService {
|
||||
});
|
||||
}
|
||||
|
||||
public areRequiredSelectorsValid(
|
||||
displaySetSelectors: HangingProtocol.DisplaySetSelector,
|
||||
displaySet: any
|
||||
): boolean {
|
||||
let pass = true;
|
||||
for (const displaySetSelector of displaySetSelectors) {
|
||||
try {
|
||||
this._validateRequiredSelectors(displaySetSelector, displaySet);
|
||||
} catch (error) {
|
||||
pass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
private _validateRequiredSelectors(
|
||||
displaySetSelector: HangingProtocol.DisplaySetSelector,
|
||||
displaySet: any
|
||||
|
||||
@@ -49,9 +49,19 @@ export default class PanelService extends PubSubService {
|
||||
const entry = this._extensionManager.getModuleEntry(panelId);
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`${panelId} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
|
||||
);
|
||||
// Check for similar panel names
|
||||
const similarPanels = this._getSimilarPanels(panelId);
|
||||
|
||||
if (similarPanels.length > 0) {
|
||||
const suggestion = `Did you mean: ${similarPanels.join(', ')}?`;
|
||||
throw new Error(
|
||||
`${panelId} is not a valid entry for an extension module. ${suggestion} Please check your configuration or make sure the extension is registered.`
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`${panelId} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry?.component) {
|
||||
@@ -65,6 +75,27 @@ export default class PanelService extends PubSubService {
|
||||
return { entry, content };
|
||||
}
|
||||
|
||||
private _getSimilarPanels(panelId: string, threshold = 0.8): string[] {
|
||||
const registeredPanels = Object.keys(this._extensionManager.modulesMap).filter(name =>
|
||||
name.includes('panelModule')
|
||||
);
|
||||
|
||||
const similarPanels = registeredPanels.filter(registeredPanelId => {
|
||||
const similarity = this._calculateSimilarity(panelId, registeredPanelId);
|
||||
return similarity >= threshold;
|
||||
});
|
||||
|
||||
return similarPanels;
|
||||
}
|
||||
|
||||
private _calculateSimilarity(str1: string, str2: string): number {
|
||||
const set1 = new Set(str1.toLowerCase().split(''));
|
||||
const set2 = new Set(str2.toLowerCase().split(''));
|
||||
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
||||
const union = new Set([...set1, ...set2]);
|
||||
return intersection.size / union.size;
|
||||
}
|
||||
|
||||
public getPanelData(panelId): PanelData {
|
||||
let content, entry;
|
||||
if (Array.isArray(panelId)) {
|
||||
|
||||
@@ -150,6 +150,7 @@ class ViewportGridService extends PubSubService {
|
||||
layoutType = 'grid',
|
||||
activeViewportId = undefined,
|
||||
findOrCreateViewport = undefined,
|
||||
isHangingProtocolLayout = false,
|
||||
}) {
|
||||
this.serviceImplementation._setLayout({
|
||||
numCols,
|
||||
@@ -158,6 +159,7 @@ class ViewportGridService extends PubSubService {
|
||||
layoutType,
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
});
|
||||
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
|
||||
numCols,
|
||||
|
||||
@@ -116,6 +116,10 @@ const getPresentationIds = (viewport, viewports): PresentationIds => {
|
||||
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);
|
||||
@@ -124,7 +128,7 @@ const getPresentationIds = (viewport, viewports): PresentationIds => {
|
||||
// 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.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
|
||||
v.viewportId === viewport.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
@@ -10,7 +10,9 @@ sidebar_label: FAQ
|
||||
- [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)
|
||||
@@ -65,14 +67,24 @@ 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).
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Migration Guides",
|
||||
"position": 11
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
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.
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
sidebar_label: Migration Guide
|
||||
sidebar_position: 2
|
||||
sidebar_label: 2.x -> 3.5
|
||||
---
|
||||
|
||||
# Migration Guide
|
||||
@@ -57,7 +57,7 @@ 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
|
||||
- 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.
|
||||
@@ -70,14 +70,14 @@ are configuration objects that will be used by the viewer to load the modules. T
|
||||
- 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)
|
||||
- [OHIF CLI](../development/ohif-cli.md)
|
||||
- [New Rendering Engine and Toolings](https://www.cornerstonejs.org/)
|
||||
- [Modes](./platform/modes/index.md)
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
@@ -90,7 +90,7 @@ Since the platform/viewer (@ohif/viewer) is already at v4.12.51, we opted to ren
|
||||
## 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)).
|
||||
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.
|
||||
@@ -100,11 +100,11 @@ There are various configurations available to customize the viewer. Each configu
|
||||
:::
|
||||
|
||||
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).
|
||||
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 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`.
|
||||
@@ -118,7 +118,7 @@ you as the user can focus on creating modes having your own use case and configu
|
||||
|
||||
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))
|
||||
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.
|
||||
@@ -184,7 +184,7 @@ for this mode. The `tmtv` mode is using the `cs3d` extension for rendering and t
|
||||
|
||||
Below you can see a screen shot from the demo showcasing 3 modes for the opened study.
|
||||
|
||||

|
||||

|
||||
|
||||
:::tip
|
||||
How do I decide certain thing should go inside a mode or extension, Here are some considerations to help you make the decision:
|
||||
@@ -222,7 +222,7 @@ Can I register a custom route to OHIF v3?
|
||||
</summary>
|
||||
|
||||
Yes, you can take advantage of the customizationService and register your own routes.
|
||||
see [custom routes](./platform/services/ui/customization-service.md#customroutes)
|
||||
see [custom routes](../platform/services/ui/customization-service.md#customroutes)
|
||||
|
||||
|
||||
</details>
|
||||
@@ -364,7 +364,7 @@ How to remove an "core" extension/mode?
|
||||
</summary>
|
||||
|
||||
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)
|
||||
about the cli tool [here](../development/ohif-cli.md)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -514,7 +514,7 @@ 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)).
|
||||
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
|
||||
@@ -667,7 +667,7 @@ By using the updated toolbarModule in OHIF v3, you can define and add toolbar bu
|
||||
|
||||
An example of split button icon in v3 is shown below
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary>
|
||||
@@ -874,7 +874,7 @@ We have recently transitioned from bundling all the extensions and the viewer in
|
||||
- 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.
|
||||
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
|
||||
@@ -233,7 +233,7 @@ differently.
|
||||
<tr>
|
||||
<td align="left">
|
||||
<a href="./modules/layout-template">
|
||||
LayoutTemplate (NEW)
|
||||
LayoutTemplate
|
||||
</a>
|
||||
</td>
|
||||
<td align="left">Control Layout of a route</td>
|
||||
@@ -241,7 +241,7 @@ differently.
|
||||
<tr>
|
||||
<td align="left">
|
||||
<a href="./modules/data-source">
|
||||
DataSource (NEW)
|
||||
DataSource
|
||||
</a>
|
||||
</td>
|
||||
<td align="left">Control the mapping from DICOM metadata to OHIF-metadata</td>
|
||||
|
||||
@@ -125,7 +125,7 @@ The following evaluators are provided by us:
|
||||
|
||||
|
||||
Sometime you want to use the same `evaluator` for different purposes, in that case you can use an object
|
||||
with `name` and `options` properties. For example, in `'evaluate.cornerstone.segmentation'` we use
|
||||
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`
|
||||
)
|
||||
@@ -133,9 +133,7 @@ this pattern, where multiple toolbar buttons are using the same evaluator but wi
|
||||
```js
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: {
|
||||
toolNames: ['CircleBrush' , 'SphereBrush']
|
||||
},
|
||||
toolNames: ['CircleBrush' , 'SphereBrush']
|
||||
},
|
||||
```
|
||||
|
||||
@@ -392,7 +390,7 @@ three different modes
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'] },
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
},
|
||||
icon: 'icon-tool-shape',
|
||||
commands: _createSetToolActiveCommands('CircleScissor'),
|
||||
@@ -425,7 +423,7 @@ We use this for brush radius change
|
||||
label: 'Brush',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||
toolNames: ['CircularBrush', 'SphereBrush'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
commands: _createSetToolActiveCommands('CircularBrush'),
|
||||
|
||||
@@ -40,7 +40,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./data/DicomMetadataStore">
|
||||
DicomMetadataStore (NEW)
|
||||
DicomMetadataStore
|
||||
</a>
|
||||
</td>
|
||||
<td>Data Service</td>
|
||||
@@ -51,7 +51,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./data/DisplaySetService">
|
||||
DisplaySetService (NEW)
|
||||
DisplaySetService
|
||||
</a>
|
||||
</td>
|
||||
<td>Data Service</td>
|
||||
@@ -62,7 +62,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./data/SegmentationService">
|
||||
segmentationService (NEW)
|
||||
segmentationService
|
||||
</a>
|
||||
</td>
|
||||
<td>Segmentation Service</td>
|
||||
@@ -73,7 +73,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./data/HangingProtocolService">
|
||||
HangingProtocolService (NEW)
|
||||
HangingProtocolService
|
||||
</a>
|
||||
</td>
|
||||
<td>Data Service</td>
|
||||
@@ -95,7 +95,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./data/ToolBarService">
|
||||
ToolBarService (NEW)
|
||||
ToolBarService
|
||||
</a>
|
||||
</td>
|
||||
<td>Data Service</td>
|
||||
@@ -106,7 +106,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./ui/viewport-grid-service">
|
||||
ViewportGridService (NEW)
|
||||
ViewportGridService
|
||||
</a>
|
||||
</td>
|
||||
<td>UI Service</td>
|
||||
@@ -117,7 +117,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./ui/cine-service">
|
||||
Cine Service (NEW)
|
||||
Cine Service
|
||||
</a>
|
||||
</td>
|
||||
<td>UI Service</td>
|
||||
@@ -133,7 +133,7 @@ The following services is available in the `OHIF-v3`.
|
||||
</td>
|
||||
<td>UI Service</td>
|
||||
<td>
|
||||
customizationService (NEW)
|
||||
customizationService
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -172,7 +172,7 @@ The following services is available in the `OHIF-v3`.
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./ui/ui-viewport-dialog-service">
|
||||
UIViewportDialogService (NEW)
|
||||
UIViewportDialogService
|
||||
</a>
|
||||
</td>
|
||||
<td>UI Service</td>
|
||||
|
||||
@@ -11,6 +11,16 @@ 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:
|
||||
|
||||
@@ -284,7 +284,7 @@ html[data-theme='dark'] .docusaurus-highlight-code-line {
|
||||
}
|
||||
|
||||
.new-badge::after {
|
||||
content: ' (NEW)';
|
||||
content: '';
|
||||
@apply bg-red-300 text-red-500;
|
||||
@apply dark:bg-blue-900 dark:text-blue-100;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../Icon/Icon';
|
||||
|
||||
function LayoutPreset({ onSelection, title, icon, commandOptions, classNames }) {
|
||||
function LayoutPreset({
|
||||
onSelection,
|
||||
title,
|
||||
icon,
|
||||
commandOptions,
|
||||
classNames: classNameProps,
|
||||
disabled,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames}
|
||||
className={classNames(classNameProps, disabled && 'ohif-disabled')}
|
||||
onClick={() => {
|
||||
onSelection(commandOptions);
|
||||
}}
|
||||
@@ -29,6 +37,7 @@ LayoutPreset.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
commandOptions: PropTypes.object.isRequired,
|
||||
classNames: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LayoutPreset;
|
||||
@@ -7,7 +7,7 @@ function AddSegmentRow({ onClick, onToggleSegmentationVisibility = null, segment
|
||||
return (
|
||||
<div className="flex justify-between bg-black pl-[34px] hover:cursor-pointer">
|
||||
<div
|
||||
className="group pt-[5px] pb-[5px]"
|
||||
className="group py-[5px] pb-[5px]"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pr-2">
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from 'react';
|
||||
import Icon from '../Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function NoSegmentationRow({ onSegmentationAdd }) {
|
||||
function NoSegmentationRow({ onSegmentationAdd, addSegmentationClassName }) {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
return (
|
||||
<div
|
||||
className="group"
|
||||
className={`group ${addSegmentationClassName}`}
|
||||
onClick={onSegmentationAdd}
|
||||
>
|
||||
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] group-hover:cursor-pointer">
|
||||
|
||||
@@ -15,6 +15,7 @@ function SegmentationDropDownRow({
|
||||
storeSegmentation,
|
||||
onSegmentationDelete,
|
||||
onSegmentationAdd,
|
||||
addSegmentationClassName,
|
||||
}) {
|
||||
const handleChange = option => {
|
||||
onActiveSegmentationChange(option.value); // Notify the parent
|
||||
@@ -41,7 +42,7 @@ function SegmentationDropDownRow({
|
||||
id="segmentation-dropdown"
|
||||
showDropdownIcon={false}
|
||||
alignment="left"
|
||||
itemsClassName="text-primary-active"
|
||||
itemsClassName={`text-primary-active ${addSegmentationClassName}`}
|
||||
showBorders={false}
|
||||
maxCharactersPerLine={30}
|
||||
list={[
|
||||
|
||||
@@ -42,6 +42,7 @@ const SegmentationGroupTable = ({
|
||||
setRenderFill,
|
||||
setRenderInactiveSegmentations,
|
||||
setRenderOutline,
|
||||
addSegmentationClassName,
|
||||
}) => {
|
||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
|
||||
@@ -100,9 +101,12 @@ const SegmentationGroupTable = ({
|
||||
)}
|
||||
<div className="bg-primary-dark ">
|
||||
{segmentations?.length === 0 ? (
|
||||
<div className="select-none bg-black pt-[5px] pb-[5px]">
|
||||
<div className="select-none bg-black py-[3px]">
|
||||
{showAddSegmentation && !disableEditing && (
|
||||
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
|
||||
<NoSegmentationRow
|
||||
onSegmentationAdd={onSegmentationAdd}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -118,6 +122,7 @@ const SegmentationGroupTable = ({
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationAdd={onSegmentationAdd}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
/>
|
||||
{!disableEditing && showAddSegment && (
|
||||
|
||||
@@ -51,7 +51,7 @@ const Thumbnail = ({
|
||||
<div ref={drag}>
|
||||
<div
|
||||
className={classnames(
|
||||
'min-h-32 flex flex-1 items-center justify-center overflow-hidden rounded-md bg-black text-base text-white',
|
||||
'flex h-32 flex-1 items-center justify-center overflow-hidden rounded-md bg-black text-base text-white',
|
||||
isActive
|
||||
? 'border-primary-light border-2'
|
||||
: 'border-secondary-light border hover:border-blue-300'
|
||||
@@ -64,7 +64,7 @@ const Thumbnail = ({
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={imageAltText}
|
||||
className="min-h-32 object-contain"
|
||||
className="h-full w-full object-contain"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -16,7 +16,7 @@ const ThumbnailList = ({
|
||||
return (
|
||||
<div
|
||||
id="ohif-thumbnail-list"
|
||||
className="ohif-scrollbar study-min-height overflow-y-hidden bg-black py-3"
|
||||
className="ohif-scrollbar study-min-height overflow-y-hidden bg-black py-5"
|
||||
>
|
||||
{thumbnails.map(
|
||||
({
|
||||
|
||||
@@ -32,10 +32,7 @@ function ThumbnailTracked({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex flex-1 cursor-pointer flex-row px-3 py-2 outline-none',
|
||||
className
|
||||
)}
|
||||
className={classnames('flex flex-1 cursor-pointer flex-row px-3 outline-none', className)}
|
||||
id={`thumbnail-${displaySetInstanceUID}`}
|
||||
>
|
||||
<div className="flex-2 flex flex-col items-center">
|
||||
|
||||
@@ -65,6 +65,7 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
|
||||
cmds.forEach(command => {
|
||||
const isString = typeof command === 'string';
|
||||
const isObject = typeof command === 'object';
|
||||
const isFunction = typeof command === 'function';
|
||||
|
||||
if (isString) {
|
||||
commandsManager.run(command, { value });
|
||||
@@ -73,6 +74,8 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
|
||||
...command,
|
||||
commandOptions: { ...command.commandOptions, ...option, value },
|
||||
});
|
||||
} else if (isFunction) {
|
||||
command({ value, commandsManager, servicesManager });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function CineProvider({ children, service }) {
|
||||
setCine,
|
||||
setIsCineEnabled: isCineEnabled => service.setIsCineEnabled(isCineEnabled),
|
||||
playClip: (element, playClipOptions) => service.playClip(element, playClipOptions),
|
||||
stopClip: element => service.stopClip(element),
|
||||
stopClip: (element, stopClipOptions) => service.stopClip(element, stopClipOptions),
|
||||
};
|
||||
|
||||
return <CineContext.Provider value={[state, api]}>{children}</CineContext.Provider>;
|
||||
|
||||
@@ -28,6 +28,7 @@ interface Layout {
|
||||
interface DefaultState {
|
||||
activeViewportId: string | null;
|
||||
layout: Layout;
|
||||
isHangingProtocolLayout: boolean;
|
||||
viewports: Map<string, Viewport>;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,16 @@ const DEFAULT_STATE: DefaultState = {
|
||||
numCols: 0,
|
||||
layoutType: 'grid',
|
||||
},
|
||||
// this flag is used to determine if the hanging protocol layout is active
|
||||
// so that we can inherit the viewport options from the previous state
|
||||
// otherwise we will not allow that. Basically the issue is that we need
|
||||
// to be able to come out of the hanging protocol layout and go back to the
|
||||
// regular layout e.g., if we are in the MPR hanging protocol, and someone use
|
||||
// 1x1 layout by custom layout selector, there is no way to drag and drop
|
||||
// a non-reconstructible series to the viewport since it will always
|
||||
// inherit the hanging protocol layout options (volume viewport),
|
||||
// so we need to be able to switch back to the regular layout.
|
||||
isHangingProtocolLayout: false,
|
||||
// Viewports structure has been changed to Map (previously it was
|
||||
// tied to the viewportIndex which caused multiple issues. Now we have
|
||||
// moved completely to viewportId which is unique for each viewport.
|
||||
@@ -152,7 +163,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
// Use the newly provide viewportOptions and display set options
|
||||
// when provided, and otherwise fall back to the previous ones.
|
||||
// That allows for easy updates of just the display set.
|
||||
const viewportOptions = merge(
|
||||
let viewportOptions = merge(
|
||||
{},
|
||||
previousViewport?.viewportOptions,
|
||||
updatedViewport?.viewportOptions
|
||||
@@ -161,18 +172,29 @@ export function ViewportGridProvider({ children, service }) {
|
||||
const displaySetOptions = updatedViewport.displaySetOptions || [];
|
||||
if (!displaySetOptions.length) {
|
||||
// Copy all the display set options, assuming a full set of displaySet UID's is provided.
|
||||
displaySetOptions.push(...previousViewport.displaySetOptions);
|
||||
if (state.isHangingProtocolLayout) {
|
||||
displaySetOptions.push(...(previousViewport.displaySetOptions || []));
|
||||
}
|
||||
if (!displaySetOptions.length) {
|
||||
displaySetOptions.push({});
|
||||
}
|
||||
}
|
||||
|
||||
// if it is not part of the hanging protocol layout, we should remove the toolGroupId
|
||||
// and viewportType from the viewportOptions so that it doesn't
|
||||
// inherit the hanging protocol layout options
|
||||
if (!state.isHangingProtocolLayout) {
|
||||
viewportOptions = {
|
||||
viewportId: viewportOptions.viewportId,
|
||||
};
|
||||
}
|
||||
|
||||
const newViewport = {
|
||||
...previousViewport,
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
viewportLabel: getViewportLabel(viewports, viewportId),
|
||||
// viewportLabel: getViewportLabel(viewports, viewportId),
|
||||
};
|
||||
|
||||
viewportOptions.presentationIds = ViewportGridService.getPresentationIds(
|
||||
@@ -196,6 +218,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
layoutType = 'grid',
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
} = action.payload;
|
||||
|
||||
// If empty viewportOptions, we use numRow and numCols to calculate number of viewports
|
||||
@@ -283,6 +306,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
layoutType,
|
||||
},
|
||||
viewports,
|
||||
isHangingProtocolLayout,
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
@@ -373,6 +397,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
layoutOptions = [],
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
}) =>
|
||||
dispatch({
|
||||
type: 'SET_LAYOUT',
|
||||
@@ -383,6 +408,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
layoutOptions,
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
|
||||
Reference in new issue
Block a user