fix(viewport): Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings (#4037)
This commit is contained in:
parent
a6c6fff43a
commit
f99a0bfb31
@ -116,7 +116,10 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.70.5",
|
||||
"@cornerstonejs/core": "^1.70.5",
|
||||
"@kitware/vtk.js": "30.3.1",
|
||||
"@cornerstonejs/adapters": "^1.70.6",
|
||||
"@cornerstonejs/core": "^1.70.6",
|
||||
"@kitware/vtk.js": "30.3.3",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -434,30 +434,6 @@ const commandsModule = ({
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleThresholdRangeAndDynamic() {
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
|
||||
if (!toolGroupIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id);
|
||||
|
||||
brushInstances.forEach(({ configuration }) => {
|
||||
const { activeStrategy, strategySpecificConfiguration } = configuration;
|
||||
|
||||
if (activeStrategy.startsWith('THRESHOLD')) {
|
||||
const thresholdConfig = strategySpecificConfiguration.THRESHOLD;
|
||||
|
||||
if (thresholdConfig) {
|
||||
thresholdConfig.isDynamic = !thresholdConfig.isDynamic;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -491,9 +467,6 @@ const commandsModule = ({
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
toggleThresholdRangeAndDynamic: {
|
||||
commandFn: actions.toggleThresholdRangeAndDynamic,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -59,12 +59,8 @@ function getToolNameForButton(button) {
|
||||
const commands = props?.commands || button.commands;
|
||||
const commandsArray = Array.isArray(commands) ? commands : [commands];
|
||||
const firstCommand = commandsArray[0];
|
||||
if (typeof firstCommand === 'string') {
|
||||
// likely not a cornerstone tool
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('commandOptions' in firstCommand) {
|
||||
if (firstCommand?.commandOptions) {
|
||||
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
|
||||
}
|
||||
|
||||
|
||||
@ -18,11 +18,13 @@ export default function PanelSegmentation({
|
||||
extensionManager,
|
||||
configuration,
|
||||
}) {
|
||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
||||
const { segmentationService, viewportGridService, uiDialogService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
|
||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
@ -52,6 +54,52 @@ export default function PanelSegmentation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 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 changed = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
|
||||
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
|
||||
|
||||
const subs = [];
|
||||
[ready, changed].forEach(evt => {
|
||||
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
|
||||
handleActiveViewportChange(viewportId);
|
||||
});
|
||||
|
||||
subs.push(unsubscribe);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
subs.forEach(unsub => unsub());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
@ -152,6 +200,7 @@ export default function PanelSegmentation({
|
||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
// segment hide
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
@ -257,6 +306,7 @@ export default function PanelSegmentation({
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAddWrapper}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
showAddSegment={allowAddSegment}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
|
||||
@ -112,7 +112,10 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.70.5",
|
||||
"@cornerstonejs/core": "^1.70.5",
|
||||
"@cornerstonejs/tools": "^1.70.5",
|
||||
"@cornerstonejs/adapters": "^1.70.6",
|
||||
"@cornerstonejs/core": "^1.70.6",
|
||||
"@cornerstonejs/tools": "^1.70.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
|
||||
// Filter toolData to only render the data for the active SR.
|
||||
const filteredAnnotations = annotations.filter(annotation =>
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
|
||||
);
|
||||
|
||||
if (!viewport._actors?.size) {
|
||||
@ -82,8 +82,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||
const annotation = filteredAnnotations[i];
|
||||
const annotationUID = annotation.annotationUID;
|
||||
const { renderableData } = annotation.data.cachedStats;
|
||||
const { cachedStats } = annotation.data;
|
||||
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
|
||||
const { referencedImageId } = annotation.metadata;
|
||||
|
||||
styleSpecifier.annotationUID = annotationUID;
|
||||
@ -95,7 +94,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||
const color =
|
||||
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
? 'rgb(0, 255, 0)'
|
||||
: this.getStyle('color', styleSpecifier, annotation);
|
||||
|
||||
|
||||
@ -51,10 +51,9 @@ export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameN
|
||||
handles: {
|
||||
textBox: measurement.textBox ?? {},
|
||||
},
|
||||
cachedStats: {
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
},
|
||||
cachedStats: {},
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
frameNumber,
|
||||
},
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
|
||||
@ -203,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// The positionIds for the viewport aren't meaningful for the child display sets
|
||||
positionIds: null,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
@ -378,6 +382,10 @@ async function _getViewportReferencedDisplaySetData(
|
||||
measurementSelected,
|
||||
displaySetService
|
||||
) {
|
||||
const { measurements } = displaySet;
|
||||
const measurement = measurements[measurementSelected];
|
||||
|
||||
const { displaySetInstanceUID } = measurement;
|
||||
if (!displaySet.keyImageDisplaySet) {
|
||||
// Create a new display set, and preserve a reference to it here,
|
||||
// so that it can be re-displayed and shown inside the SR viewport.
|
||||
|
||||
@ -42,9 +42,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^1.70.5",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.70.5",
|
||||
"@cornerstonejs/tools": "^1.70.5",
|
||||
"@cornerstonejs/core": "^1.70.6",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.70.6",
|
||||
"@cornerstonejs/tools": "^1.70.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ const DynamicVolumeControls = ({
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(false);
|
||||
onDynamicClick();
|
||||
onDynamicClick?.();
|
||||
}}
|
||||
>
|
||||
4D
|
||||
|
||||
@ -202,7 +202,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager })
|
||||
}
|
||||
|
||||
const { element } = viewportInfo;
|
||||
cineService.playClip(element, { framesPerSecond: frameRate });
|
||||
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
|
||||
@ -38,7 +38,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",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.8.0-beta.80",
|
||||
"@ohif/ui": "3.8.0-beta.80",
|
||||
@ -55,12 +55,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.70.5",
|
||||
"@cornerstonejs/core": "^1.70.5",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.70.5",
|
||||
"@cornerstonejs/tools": "^1.70.5",
|
||||
"@cornerstonejs/adapters": "^1.70.6",
|
||||
"@cornerstonejs/core": "^1.70.6",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.70.6",
|
||||
"@cornerstonejs/tools": "^1.70.6",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "30.3.1",
|
||||
"@kitware/vtk.js": "30.3.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
||||
@ -114,10 +114,21 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
initialImageIndex,
|
||||
// if the viewport is part of a hanging protocol layout
|
||||
// we should not really rely on the old synchronizers and
|
||||
// you see below we only rehydrate the synchronizers if the viewport
|
||||
// is not part of the hanging protocol layout. HPs should
|
||||
// define their own synchronizers. Since the synchronizers are
|
||||
// viewportId dependent and
|
||||
isHangingProtocolLayout,
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
if (!viewportId) {
|
||||
throw new Error('Viewport ID is required');
|
||||
}
|
||||
|
||||
// Since we only have support for dynamic data in volume viewports, we should
|
||||
// handle this case here and set the viewportType to volume if any of the
|
||||
// displaySets are dynamic volumes
|
||||
@ -194,7 +205,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
const synchronizersStore = stateSyncService.getState().synchronizersStore;
|
||||
|
||||
if (synchronizersStore?.[viewportId]?.length) {
|
||||
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
||||
// If the viewport used to have a synchronizer, re apply it again
|
||||
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
|
||||
}
|
||||
|
||||
@ -51,6 +51,12 @@ function commandsModule({
|
||||
function _getActiveViewportEnabledElement() {
|
||||
return getActiveViewportEnabledElement(viewportGridService);
|
||||
}
|
||||
|
||||
function _getActiveViewportToolGroupId() {
|
||||
const viewport = _getActiveViewportEnabledElement();
|
||||
return toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Generates the selector props for the context menu, specific to
|
||||
@ -300,42 +306,45 @@ function commandsModule({
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
toggleEnabledDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||
toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
|
||||
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
});
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
},
|
||||
toggleActiveDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||
toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsActive = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Active;
|
||||
const toolIsActive = [
|
||||
Enums.ToolModes.Active,
|
||||
Enums.ToolModes.Enabled,
|
||||
Enums.ToolModes.Passive,
|
||||
].includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
toolIsActive
|
||||
? toolGroup.setToolDisabled(toolName)
|
||||
: actions.setToolActive({ toolName, toolGroupId });
|
||||
toolIsActive
|
||||
? toolGroup.setToolDisabled(toolName)
|
||||
: actions.setToolActive({ toolName, toolGroupId });
|
||||
|
||||
// we should set the previously active tool to active after we set the
|
||||
// current tool disabled
|
||||
if (toolIsActive) {
|
||||
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||
// we should set the previously active tool to active after we set the
|
||||
// current tool disabled
|
||||
if (toolIsActive) {
|
||||
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||
if (prevToolName !== toolName) {
|
||||
actions.setToolActive({ toolName: prevToolName, toolGroupId });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
@ -441,11 +450,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { flipHorizontal } = viewport.getCamera();
|
||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||
viewport.render();
|
||||
}
|
||||
const { flipHorizontal } = viewport.getCamera();
|
||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||
viewport.render();
|
||||
},
|
||||
flipViewportVertical: () => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
@ -456,11 +463,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { flipVertical } = viewport.getCamera();
|
||||
viewport.setCamera({ flipVertical: !flipVertical });
|
||||
viewport.render();
|
||||
}
|
||||
const { flipVertical } = viewport.getCamera();
|
||||
viewport.setCamera({ flipVertical: !flipVertical });
|
||||
viewport.render();
|
||||
},
|
||||
invertViewport: ({ element }) => {
|
||||
let enabledElement;
|
||||
@ -819,7 +824,7 @@ function commandsModule({
|
||||
}
|
||||
|
||||
crosshairInstances.forEach(ins => {
|
||||
ins.resetCrosshairs();
|
||||
ins?.resetCrosshairs();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
return isPlaying
|
||||
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate })
|
||||
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId })
|
||||
: cineService.stopClip(enabledVPElement);
|
||||
};
|
||||
|
||||
|
||||
@ -108,32 +108,27 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
|
||||
evaluate: ({ viewportId, button, disabledText }) =>
|
||||
_evaluateToggle({
|
||||
viewportId,
|
||||
button,
|
||||
disabledText,
|
||||
offModes: [Enums.ToolModes.Disabled],
|
||||
toolGroupService,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle',
|
||||
evaluate: ({ viewportId, button, disabledText }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
|
||||
toolGroup.getToolOptions(toolName).mode
|
||||
);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
},
|
||||
evaluate: ({ viewportId, button, disabledText }) =>
|
||||
_evaluateToggle({
|
||||
viewportId,
|
||||
button,
|
||||
disabledText,
|
||||
offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive],
|
||||
toolGroupService,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.synchronizer',
|
||||
@ -146,7 +141,11 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
}
|
||||
|
||||
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
|
||||
const isArray = Array.isArray(button.commands);
|
||||
|
||||
const synchronizerType = isArray
|
||||
? button.commands?.[0].commandOptions.type
|
||||
: button.commands?.commandOptions.type;
|
||||
|
||||
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
|
||||
|
||||
@ -268,18 +267,38 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
];
|
||||
}
|
||||
|
||||
function _evaluateToggle({ viewportId, button, disabledText, offModes, toolGroupService }) {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: this is duplicate, we should move it to a shared location
|
||||
function getToolNameForButton(button) {
|
||||
const { props } = button;
|
||||
|
||||
const commands = props?.commands || button.commands;
|
||||
const commandsArray = Array.isArray(commands) ? commands : [commands];
|
||||
const firstCommand = commandsArray[0];
|
||||
if (typeof firstCommand === 'string') {
|
||||
// likely not a cornerstone tool
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('commandOptions' in firstCommand) {
|
||||
if (firstCommand?.commandOptions) {
|
||||
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +61,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
onModeExit: (): void => {
|
||||
onModeExit: ({ servicesManager }): void => {
|
||||
const { cineService } = servicesManager.services;
|
||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||
// on the mode exits
|
||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||
@ -69,6 +70,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
cineService.setIsCineEnabled(false);
|
||||
|
||||
enabledElementReset();
|
||||
},
|
||||
|
||||
|
||||
@ -34,6 +34,8 @@ import { CornerstoneServices } from './types';
|
||||
import initViewTiming from './utils/initViewTiming';
|
||||
import { colormaps } from './utils/colormaps';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const { registerColormap } = csUtilities.colormap;
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
@ -291,8 +293,21 @@ export default async function init({
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
|
||||
colormaps.forEach(registerColormap);
|
||||
|
||||
// Create a debounced function that shows the notification
|
||||
const debouncedShowNotification = debounce(detail => {
|
||||
uiNotificationService.show({
|
||||
title: detail.type,
|
||||
message: detail.message,
|
||||
type: 'error',
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// Event listener
|
||||
eventTarget.addEventListener(EVENTS.ERROR_EVENT, ({ detail }) => {
|
||||
debouncedShowNotification(detail);
|
||||
});
|
||||
}
|
||||
|
||||
function CPUModal() {
|
||||
|
||||
@ -59,8 +59,8 @@ function initCineService(servicesManager) {
|
||||
return utilities.cine.playClip(element, playClipOptions);
|
||||
};
|
||||
|
||||
const stopClip = element => {
|
||||
return utilities.cine.stopClip(element);
|
||||
const stopClip = (element, stopClipOptions) => {
|
||||
return utilities.cine.stopClip(element, stopClipOptions);
|
||||
};
|
||||
|
||||
cineService.setServiceImplementation({
|
||||
|
||||
@ -45,7 +45,11 @@ class CornerstoneCacheService {
|
||||
// as a reference volume, if so, we should hang a volume viewport
|
||||
// instead of a stack viewport
|
||||
if (this._shouldRenderSegmentation(displaySets)) {
|
||||
viewportType = 'volume';
|
||||
// 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;
|
||||
|
||||
@ -183,7 +183,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const { lutPresentation, positionPresentation } = presentations;
|
||||
if (lutPresentation) {
|
||||
const { presentation } = lutPresentation;
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
if (viewport instanceof BaseVolumeViewport) {
|
||||
if (presentation instanceof Map) {
|
||||
presentation.forEach((properties, volumeId) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
|
||||
@ -27,7 +27,11 @@ const CornerstoneViewportDownloadForm = ({
|
||||
const activeViewportElement = enabledElement?.element;
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
|
||||
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
|
||||
const {
|
||||
viewportId: activeViewportId,
|
||||
renderingEngineId,
|
||||
viewport: activeViewport,
|
||||
} = activeViewportEnabledElement;
|
||||
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
|
||||
|
||||
@ -93,7 +97,7 @@ const CornerstoneViewportDownloadForm = ({
|
||||
renderingEngine.resize();
|
||||
|
||||
// Trigger the render on the viewport to update the on screen
|
||||
downloadViewport.resetCamera();
|
||||
// downloadViewport.resetCamera();
|
||||
downloadViewport.render();
|
||||
|
||||
downloadViewportElement.addEventListener(
|
||||
@ -120,6 +124,12 @@ const CornerstoneViewportDownloadForm = ({
|
||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||
|
||||
downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport);
|
||||
|
||||
// for some reason we need a reset camera here, and I don't know why
|
||||
downloadViewport.resetCamera();
|
||||
const presentation = activeViewport.getViewPresentation();
|
||||
downloadViewport.setView(activeViewport.getViewReference(), presentation);
|
||||
downloadViewport.render();
|
||||
}
|
||||
);
|
||||
});
|
||||
@ -161,7 +171,6 @@ const CornerstoneViewportDownloadForm = ({
|
||||
downloadViewport.addActor(actor);
|
||||
});
|
||||
|
||||
downloadViewport.setCamera(viewport.getCamera());
|
||||
downloadViewport.render();
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
|
||||
@ -51,20 +51,27 @@ export default function interleaveCenterLoader({
|
||||
* listen to it and as the other viewports are created we can set the volumes for them
|
||||
* since volumes are already started loading.
|
||||
*/
|
||||
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
||||
const uniqueViewportVolumeDisplaySetUIDs = new Set();
|
||||
viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => {
|
||||
volumeInputArray.forEach(volumeInput => {
|
||||
const { volumeId } = volumeInput;
|
||||
uniqueViewportVolumeDisplaySetUIDs.add(volumeId);
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueMatchedDisplaySetUIDs = new Set();
|
||||
|
||||
matchDetails.forEach(matchDetail => {
|
||||
const { displaySetsInfo } = matchDetail;
|
||||
displaySetsInfo.forEach(({ displaySetInstanceUID }) => {
|
||||
uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID);
|
||||
});
|
||||
});
|
||||
|
||||
if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all the matched volumes are loaded
|
||||
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||
const { SeriesInstanceUID } = details;
|
||||
|
||||
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||
// get volumes from cache
|
||||
const volumes = volumeIds.map(volumeId => {
|
||||
|
||||
@ -75,23 +75,27 @@ export default function interleaveTopToBottom({
|
||||
* listen to it and as the other viewports are created we can set the volumes for them
|
||||
* since volumes are already started loading.
|
||||
*/
|
||||
if (filteredMatchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||
const uniqueViewportVolumeDisplaySetUIDs = new Set();
|
||||
viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => {
|
||||
volumeInputArray.forEach(volumeInput => {
|
||||
const { volumeId } = volumeInput;
|
||||
uniqueViewportVolumeDisplaySetUIDs.add(volumeId);
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueMatchedDisplaySetUIDs = new Set();
|
||||
|
||||
matchDetails.forEach(matchDetail => {
|
||||
const { displaySetsInfo } = matchDetail;
|
||||
displaySetsInfo.forEach(({ displaySetInstanceUID }) => {
|
||||
uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID);
|
||||
});
|
||||
});
|
||||
|
||||
if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all the matched volumes are loaded
|
||||
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||
const { SeriesInstanceUID, displaySetInstanceUID } = details;
|
||||
|
||||
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||
if (
|
||||
displaySetsToLoad.has(displaySetInstanceUID) &&
|
||||
!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||
// get volumes from cache
|
||||
const volumes = volumeIds.map(volumeId => {
|
||||
|
||||
@ -12,6 +12,9 @@ const defaultContextMenu = {
|
||||
commands: [
|
||||
{
|
||||
commandName: 'deleteMeasurement',
|
||||
// we only have support for cornerstoneTools context menu since
|
||||
// they are svg based
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -227,7 +227,7 @@ export default function PanelMeasurementTable({
|
||||
</div>
|
||||
<div className="flex justify-center p-4">
|
||||
<ActionButtons
|
||||
t={t('MeasurementTable')}
|
||||
t={t}
|
||||
actions={[
|
||||
{
|
||||
label: 'Export',
|
||||
|
||||
@ -5,13 +5,8 @@
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
// Note: the default width and height of the canvas is 300x150
|
||||
// but we need to set the width and height to the same number since
|
||||
// the thumbnails are usually square and we want to maintain the aspect ratio
|
||||
canvas.width = 128 / window.devicePixelRatio;
|
||||
canvas.height = 128 / window.devicePixelRatio;
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas({ canvas, imageId })
|
||||
.loadImageToCanvas({ canvas, imageId, thumbnail: true })
|
||||
.then(imageId => {
|
||||
resolve(canvas.toDataURL());
|
||||
})
|
||||
|
||||
@ -68,7 +68,6 @@ function getSopClassUids(instances) {
|
||||
}
|
||||
|
||||
function _getDisplaySetsFromSeries(instances) {
|
||||
debugger;
|
||||
// If the series has no instances, stop here
|
||||
if (!instances || !instances.length) {
|
||||
throw new Error('No instances were provided');
|
||||
|
||||
@ -34,19 +34,47 @@ const defaultCommonPresets = [
|
||||
},
|
||||
];
|
||||
|
||||
const generateAdvancedPresets = hangingProtocolService => {
|
||||
const _areSelectorsValid = (hp, displaySets, hangingProtocolService) => {
|
||||
if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hangingProtocolService.areRequiredSelectorsValid(
|
||||
Object.values(hp.displaySetSelectors),
|
||||
displaySets[0]
|
||||
);
|
||||
};
|
||||
|
||||
const generateAdvancedPresets = ({ servicesManager }) => {
|
||||
const { hangingProtocolService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const hangingProtocols = Array.from(hangingProtocolService.protocols.values());
|
||||
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
|
||||
if (!viewportId) {
|
||||
return [];
|
||||
}
|
||||
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
const displaySets = displaySetInsaneUIDs.map(uid => displaySetService.getDisplaySetByUID(uid));
|
||||
|
||||
return hangingProtocols
|
||||
.map(hp => {
|
||||
if (!hp.isPreset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService);
|
||||
|
||||
return {
|
||||
icon: hp.icon,
|
||||
title: hp.name,
|
||||
commandOptions: {
|
||||
protocolId: hp.id,
|
||||
},
|
||||
disabled: !areValid,
|
||||
};
|
||||
})
|
||||
.filter(preset => preset !== null);
|
||||
@ -100,10 +128,10 @@ function LayoutSelector({
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { customizationService, hangingProtocolService } = servicesManager.services;
|
||||
const { customizationService } = servicesManager.services;
|
||||
const commonPresets = customizationService.get('commonPresets') || defaultCommonPresets;
|
||||
const advancedPresets =
|
||||
customizationService.get('advancedPresets') || generateAdvancedPresets(hangingProtocolService);
|
||||
customizationService.get('advancedPresets') || generateAdvancedPresets({ servicesManager });
|
||||
|
||||
const closeOnOutsideClick = () => {
|
||||
if (isOpen) {
|
||||
@ -161,6 +189,7 @@ function LayoutSelector({
|
||||
classNames="hover:bg-primary-dark group flex gap-2 p-1 cursor-pointer"
|
||||
icon={preset.icon}
|
||||
title={preset.title}
|
||||
disabled={preset.disabled}
|
||||
commandOptions={preset.commandOptions}
|
||||
onSelection={onSelectionPreset}
|
||||
/>
|
||||
|
||||
@ -271,7 +271,7 @@ const commandsModule = ({
|
||||
/**
|
||||
* Changes the viewport grid layout in terms of the MxN layout.
|
||||
*/
|
||||
setViewportGridLayout: ({ numRows, numCols }) => {
|
||||
setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => {
|
||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
||||
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
||||
@ -293,6 +293,7 @@ const commandsModule = ({
|
||||
numRows,
|
||||
numCols,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
});
|
||||
stateSyncService.store(stateReduce);
|
||||
};
|
||||
@ -366,6 +367,7 @@ const commandsModule = ({
|
||||
activeViewportId: viewportIdToUpdate,
|
||||
layoutOptions,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout: true,
|
||||
});
|
||||
} else {
|
||||
// We are not in one-up, so toggle to one up.
|
||||
@ -390,6 +392,7 @@ const commandsModule = ({
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout: true,
|
||||
});
|
||||
|
||||
// Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^1.70.5",
|
||||
"@cornerstonejs/tools": "^1.70.5",
|
||||
"@cornerstonejs/core": "^1.70.6",
|
||||
"@cornerstonejs/tools": "^1.70.6",
|
||||
"@ohif/core": "3.8.0-beta.80",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.80",
|
||||
"@ohif/ui": "3.8.0-beta.80",
|
||||
|
||||
@ -5,14 +5,8 @@
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
// Note: the default width and height of the canvas is 300x150
|
||||
// but we need to set the width and height to the same number since
|
||||
// the thumbnails are usually square and we want to maintain the aspect ratio
|
||||
canvas.width = 128 / window.devicePixelRatio;
|
||||
canvas.height = 128 / window.devicePixelRatio;
|
||||
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas({ canvas, imageId })
|
||||
.loadImageToCanvas({ canvas, imageId, thumbnail: true })
|
||||
.then(imageId => {
|
||||
resolve(canvas.toDataURL());
|
||||
})
|
||||
|
||||
@ -219,7 +219,10 @@ function TrackedCornerstoneViewport(props) {
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -47,8 +47,8 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
|
||||
if (Number.isNaN(suvPeak)) {
|
||||
uiNotificationService.show({
|
||||
title: 'Unable to calculate SUV Peak',
|
||||
message: 'The resulting threshold is not big enough.',
|
||||
title: 'SUV Peak',
|
||||
message: 'Segmented volume does not allow SUV Peak calculation',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
|
||||
{ toolName: toolNames.Angle },
|
||||
{ toolName: toolNames.Magnify },
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
{ toolName: toolNames.AdvancedMagnify },
|
||||
|
||||
{ toolName: toolNames.UltrasoundDirectional },
|
||||
{ toolName: toolNames.PlanarFreehandROI },
|
||||
{ toolName: toolNames.SplineROI },
|
||||
@ -59,7 +59,7 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
|
||||
// enabled
|
||||
enabled: [{ toolName: toolNames.ImageOverlayViewer }],
|
||||
// disabled
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }],
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }, { toolName: toolNames.AdvancedMagnify }],
|
||||
};
|
||||
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
|
||||
@ -11,13 +11,6 @@ const ReferenceLinesListeners: RunCommand = [
|
||||
},
|
||||
];
|
||||
|
||||
export const toggleEnabledDisabledToolbar = {
|
||||
commandName: 'toggleEnabledDisabledToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'],
|
||||
},
|
||||
};
|
||||
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'MoreTools',
|
||||
@ -87,7 +80,7 @@ const moreTools = [
|
||||
icon: 'tool-referenceLines',
|
||||
label: 'Reference Lines',
|
||||
tooltip: 'Show Reference Lines',
|
||||
commands: toggleEnabledDisabledToolbar,
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
listeners: {
|
||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||
@ -99,7 +92,7 @@ const moreTools = [
|
||||
icon: 'toggle-dicom-overlay',
|
||||
label: 'Image Overlay',
|
||||
tooltip: 'Toggle Image Overlay',
|
||||
commands: toggleEnabledDisabledToolbar,
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
@ -178,8 +171,8 @@ const moreTools = [
|
||||
icon: 'icon-tool-loupe',
|
||||
label: 'Loupe',
|
||||
tooltip: 'Loupe',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
commands: 'toggleActiveDisabledToolbar',
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
|
||||
}),
|
||||
createButton({
|
||||
id: 'UltrasoundDirectionalTool',
|
||||
|
||||
@ -65,19 +65,18 @@ function initDefaultToolGroup(
|
||||
{ toolName: toolNames.Magnify },
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
{ toolName: toolNames.CalibrationLine },
|
||||
{
|
||||
toolName: toolNames.AdvancedMagnify,
|
||||
configuration: {
|
||||
disableOnPassive: true,
|
||||
},
|
||||
},
|
||||
|
||||
{ toolName: toolNames.UltrasoundDirectional },
|
||||
{ toolName: toolNames.PlanarFreehandROI },
|
||||
{ toolName: toolNames.SplineROI },
|
||||
{ toolName: toolNames.LivewireContour },
|
||||
],
|
||||
// enabled
|
||||
enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }],
|
||||
disabled: [
|
||||
{
|
||||
toolName: toolNames.AdvancedMagnify,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
@ -224,11 +223,11 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager, m
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: toolNames.AdvancedMagnify,
|
||||
},
|
||||
{ toolName: toolNames.ReferenceLines },
|
||||
],
|
||||
|
||||
// enabled
|
||||
// disabled
|
||||
};
|
||||
|
||||
toolGroupService.createToolGroupAndAddTools('mpr', tools);
|
||||
|
||||
@ -11,20 +11,6 @@ const ReferenceLinesListeners: RunCommand = [
|
||||
},
|
||||
];
|
||||
|
||||
export const toggleEnabledDisabledToolbar = {
|
||||
commandName: 'toggleEnabledDisabledToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'],
|
||||
},
|
||||
};
|
||||
|
||||
export const toggleActiveDisabledToolbar = {
|
||||
commandName: 'toggleActiveDisabledToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||
},
|
||||
};
|
||||
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'MoreTools',
|
||||
@ -94,7 +80,7 @@ const moreTools = [
|
||||
icon: 'tool-referenceLines',
|
||||
label: 'Reference Lines',
|
||||
tooltip: 'Show Reference Lines',
|
||||
commands: toggleEnabledDisabledToolbar,
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
listeners: {
|
||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||
@ -106,7 +92,7 @@ const moreTools = [
|
||||
icon: 'toggle-dicom-overlay',
|
||||
label: 'Image Overlay',
|
||||
tooltip: 'Toggle Image Overlay',
|
||||
commands: toggleEnabledDisabledToolbar,
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
@ -177,8 +163,8 @@ const moreTools = [
|
||||
icon: 'icon-tool-loupe',
|
||||
label: 'Loupe',
|
||||
tooltip: 'Loupe',
|
||||
commands: toggleActiveDisabledToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
commands: 'toggleActiveDisabledToolbar',
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
|
||||
}),
|
||||
createButton({
|
||||
id: 'UltrasoundDirectionalTool',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
// Only ways that you can pass in a custom React component for render :l
|
||||
import { defaults, ToolbarService } from '@ohif/core';
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
import type { Button } from '@ohif/core/types';
|
||||
|
||||
const { createButton } = ToolbarService;
|
||||
|
||||
@ -79,7 +79,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
() => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
cineService.playClip(csViewport.element);
|
||||
cineService.playClip(csViewport.element, { viewportId });
|
||||
// cineService.setIsCineEnabled(true);
|
||||
|
||||
unsubscribe();
|
||||
|
||||
@ -24,7 +24,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Brush',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||
toolNames: ['CircularBrush', 'SphereBrush'],
|
||||
},
|
||||
commands: _createSetToolActiveCommands('CircularBrush'),
|
||||
options: [
|
||||
@ -60,9 +60,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: {
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
commands: _createSetToolActiveCommands('CircularEraser'),
|
||||
options: [
|
||||
@ -98,7 +96,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'] },
|
||||
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
},
|
||||
commands: _createSetToolActiveCommands('ThresholdCircularBrush'),
|
||||
options: [
|
||||
@ -155,7 +153,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'] },
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
},
|
||||
icon: 'icon-tool-shape',
|
||||
commands: _createSetToolActiveCommands('CircleScissor'),
|
||||
|
||||
@ -41,7 +41,6 @@ function createTools(utilityModule) {
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
|
||||
dynamicRadius: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -49,7 +48,22 @@ function createTools(utilityModule) {
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_SPHERE',
|
||||
dynamicRadius: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'ThresholdCircularBrushDynamic',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
|
||||
preview: {
|
||||
enabled: true,
|
||||
},
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
isDynamic: true,
|
||||
dynamicRadius: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ toolName: toolNames.CircleScissors },
|
||||
@ -58,10 +72,10 @@ function createTools(utilityModule) {
|
||||
{ toolName: toolNames.StackScroll },
|
||||
{ toolName: toolNames.Magnify },
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
{ toolName: toolNames.AdvancedMagnify },
|
||||
|
||||
{ toolName: toolNames.UltrasoundDirectional },
|
||||
],
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }],
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }, { toolName: toolNames.AdvancedMagnify }],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ const toolbarButtons: Button[] = [
|
||||
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'),
|
||||
@ -61,9 +61,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: {
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
commands: _createSetToolActiveCommands('CircularEraser'),
|
||||
options: [
|
||||
@ -99,7 +97,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Threshold Tool',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'] },
|
||||
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
},
|
||||
commands: _createSetToolActiveCommands('ThresholdCircularBrush'),
|
||||
options: [
|
||||
@ -138,8 +136,16 @@ const toolbarButtons: Button[] = [
|
||||
{ value: 'ThresholdDynamic', label: 'Dynamic' },
|
||||
{ value: 'ThresholdRange', label: 'Range' },
|
||||
],
|
||||
commands: {
|
||||
commandName: 'toggleThresholdRangeAndDynamic',
|
||||
commands: ({ value, commandsManager }) => {
|
||||
if (value === 'ThresholdDynamic') {
|
||||
commandsManager.run('setToolActive', {
|
||||
toolName: 'ThresholdCircularBrushDynamic',
|
||||
});
|
||||
} else {
|
||||
commandsManager.run('setToolActive', {
|
||||
toolName: 'ThresholdCircularBrush',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -171,7 +177,7 @@ const toolbarButtons: Button[] = [
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'] },
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
},
|
||||
icon: 'icon-tool-shape',
|
||||
commands: _createSetToolActiveCommands('CircleScissor'),
|
||||
|
||||
@ -13,21 +13,7 @@ const ReferenceLinesListeners: RunCommand = [
|
||||
export const setToolActiveToolbar = {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||
},
|
||||
};
|
||||
|
||||
export const toggleEnabledDisabledToolbar = {
|
||||
commandName: 'toggleEnabledDisabledToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||
},
|
||||
};
|
||||
|
||||
export const toggleActiveDisabledToolbar = {
|
||||
commandName: 'toggleActiveDisabledToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'],
|
||||
},
|
||||
};
|
||||
|
||||
@ -163,7 +149,7 @@ const toolbarButtons: Button[] = [
|
||||
icon: 'tool-referenceLines',
|
||||
label: 'Reference Lines',
|
||||
tooltip: 'Show Reference Lines',
|
||||
commands: toggleEnabledDisabledToolbar,
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
listeners: {
|
||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||
@ -175,7 +161,7 @@ const toolbarButtons: Button[] = [
|
||||
icon: 'toggle-dicom-overlay',
|
||||
label: 'Image Overlay',
|
||||
tooltip: 'Toggle Image Overlay',
|
||||
commands: toggleEnabledDisabledToolbar,
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
@ -254,8 +240,8 @@ const toolbarButtons: Button[] = [
|
||||
icon: 'icon-tool-loupe',
|
||||
label: 'Loupe',
|
||||
tooltip: 'Loupe',
|
||||
commands: toggleActiveDisabledToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
commands: 'toggleActiveDisabledToolbar',
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
|
||||
}),
|
||||
createButton({
|
||||
id: 'UltrasoundDirectionalTool',
|
||||
|
||||
@ -184,6 +184,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
|
||||
const isValid =
|
||||
modalities_list.includes('CT') &&
|
||||
study.mrn !== 'M1' &&
|
||||
modalities_list.includes('PT') &&
|
||||
!invalidModalities.some(modality => modalities_list.includes(modality)) &&
|
||||
// This is study is a 4D study with PT and CT and not a 3D study for the tmtv
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@kitware/vtk.js": "30.3.1",
|
||||
"@kitware/vtk.js": "30.3.3",
|
||||
"core-js": "^3.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
7
platform/docs/docs/conformance.md
Normal file
7
platform/docs/docs/conformance.md
Normal file
@ -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).
|
||||
|
||||
|
||||
4
platform/docs/docs/migration-guide/_category_.json
Normal file
4
platform/docs/docs/migration-guide/_category_.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Migration Guides",
|
||||
"position": 11
|
||||
}
|
||||
137
platform/docs/docs/migration-guide/from-3p7-to-3p8.md
Normal file
137
platform/docs/docs/migration-guide/from-3p7-to-3p8.md
Normal file
@ -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.
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user