fix(core): Address 3D reconstruction and Android compatibility issues and clean up 4D data mode (#4762)

This commit is contained in:
Alireza authored and GitHub committed 2025-02-04 12:25:10 -05:00
1 parent 4d06a7d286
commit 149d6d049c
32 files changed
+529 -529

No files matched your search

+1 -1
View File
@@ -54,7 +54,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^2.17.2",
"@cornerstonejs/dicom-image-loader": "^2.19.3",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.10.0-beta.91",
"@ohif/extension-cornerstone": "3.10.0-beta.91",
+1 -1
View File
@@ -37,7 +37,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^2.17.2",
"@cornerstonejs/dicom-image-loader": "^2.19.3",
"@ohif/ui": "3.10.0-beta.91",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"
@@ -285,14 +285,25 @@ export default class HangingProtocolService extends PubSubService {
* @param protocolId - the id of the protocol
* @returns protocol - the protocol with the given id
*/
public getProtocolById(protocolId: string): HangingProtocol.Protocol {
public getProtocolById(protocolId: string, caseInsensitive = true): HangingProtocol.Protocol {
if (!protocolId) {
return;
}
if (protocolId === this.protocol?.id) {
return this.protocol;
}
const protocol = this.protocols.get(protocolId);
let protocol = this.protocols.get(protocolId);
if (!protocol && caseInsensitive) {
const lowerCaseId = protocolId.toLowerCase();
for (const [key] of this.protocols) {
if (key.toLowerCase() === lowerCaseId) {
protocol = this.getProtocolById(key);
break;
}
}
}
if (!protocol) {
throw new Error(`No protocol ${protocolId} found`);
}
@@ -533,8 +544,7 @@ export default class HangingProtocolService extends PubSubService {
}
_validateProtocol(protocol: HangingProtocol.Protocol): HangingProtocol.Protocol {
protocol.id = protocol.id || protocol.name;
protocol.name = protocol.name || protocol.id;
protocol.name = protocol.name ?? protocol.id;
const { stages } = protocol;
if (!stages) {
@@ -15,7 +15,6 @@ be centered, and not draggable. They're commonly used when:
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
customComponent: The modal service allows users to provide their own Modal UI using the customComponents.
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
@@ -38,12 +37,17 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
| Implementation | Consumer |
| ---------------------------------- | --------- |
| [Modal Provider][modal-provider]\* | Modal.jsx |
| customComponent | user extensions via setServiceImplementation({customComponent: Modal}) |
`*` - Denotes maintained by OHIF
| customComponent | user extensions via `setServiceImplementation({customComponent: Modal})` |
### Custom Component
If you would like to customize the modal component that OHIF uses, you can register your own
component with the `customComponent` property.
```js
setServiceImplementation({customComponent: Modal})
```
> 3rd Party implementers may be added to this table via pull requests.
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import Numeric from '../../../../ui-next/src/components/Numeric';
import Icons from '../../../../ui-next/src/components/Icons';
import ShowcaseRow from './ShowcaseRow';
@@ -7,6 +7,9 @@ import ShowcaseRow from './ShowcaseRow';
* NumericShowcase component displays Numeric variants and examples
*/
export default function NumericShowcase() {
const [controlledValue, setControlledValue] = useState(0);
const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]);
return (
<div className="space-y-8">
{/* Basic Number Input */}
@@ -14,19 +17,26 @@ export default function NumericShowcase() {
title="Numeric - Number Input"
description="Basic number input with min, max and a label"
code={`
<Numeric.Container mode="number" min={0} max={10} onChange={onChange}>
<Numeric.Container mode="number" min={0} max={10} onChange={val => console.debug('Value changed:', val)}>
<div className="flex flex-row items-center space-x-2">
<Numeric.Label>Width</Numeric.Label>
<Numeric.NumberInput />
</div>
</Numeric.Container>
<Numeric.Container mode="number" min={0} max={100} onChange={onChange}>
<Numeric.Label className="text-secondary-foreground text-sm font-bold">Bolder</Numeric.Label>
<Numeric.Container mode="number" className="space-y-1" min={0} max={100} onChange={val => console.debug('Value changed:', val)}>
<Numeric.Label className="text-muted-foreground text-sm font-bold">Bolder</Numeric.Label>
<Numeric.NumberInput className="w-12" />
</Numeric.Container>
<Numeric.Container mode="number" className="flex flex-row items-center justify-between" onChange={onChange} min={0} value={123465789} max={10000000000000}>
<Numeric.Container
mode="number"
className="flex flex-row items-center justify-between"
onChange={val => console.debug('Value changed:', val)}
min={0}
value={123465789}
max={10000000000000}
>
<Numeric.Label className="flex flex-row items-center">
<Icons.Add />
With Icon
@@ -82,18 +92,37 @@ export default function NumericShowcase() {
title="Numeric - Single Range"
description="Single range slider with optional number input"
code={`
<Numeric.Container mode="singleRange" min={0} max={100} step={1} value={50} onChange={onChange}>
// For controlled component
const [controlledValue, setControlledValue] = useState(0);
<Numeric.Container mode="singleRange" min={0} max={100} onChange={val => console.debug('Value changed:', val)}>
<Numeric.Label>Brightness</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
<Numeric.Container mode="singleRange" min={-50} max={50} step={1} value={0} className="flex flex-row items-center" onChange={onChange}>
<Numeric.Container
mode="singleRange"
min={-50}
max={50}
step={1}
defaultValue={0}
className="flex flex-row items-center"
onChange={val => console.debug('Value changed:', val)}
>
<Numeric.Label showValue>Contrast</Numeric.Label>
<Numeric.SingleRange />
</Numeric.Container>
<Numeric.Container mode="singleRange" min={0} max={100} step={1} value={50} className="flex flex-row items-center space-x-2" onChange={onChange}>
<Numeric.Label>Something Else </Numeric.Label>
<Numeric.Container
mode="singleRange"
min={0}
max={100}
step={1}
value={controlledValue}
className="flex flex-row items-center space-x-2"
onChange={val => setControlledValue(val as number)}
>
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>`}
>
@@ -102,8 +131,6 @@ export default function NumericShowcase() {
mode="singleRange"
min={0}
max={100}
step={1}
value={50}
onChange={val => console.debug('Value changed:', val)}
>
<Numeric.Label>Brightness</Numeric.Label>
@@ -115,7 +142,7 @@ export default function NumericShowcase() {
min={-50}
max={50}
step={1}
value={0}
defaultValue={0}
className="flex flex-row items-center"
onChange={val => console.debug('Value changed:', val)}
>
@@ -128,11 +155,11 @@ export default function NumericShowcase() {
min={0}
max={100}
step={1}
value={50}
value={controlledValue}
className="flex flex-row items-center space-x-2"
onChange={val => console.debug('Value changed:', val)}
onChange={val => setControlledValue(val as number)}
>
<Numeric.Label>Something Else </Numeric.Label>
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
</div>
@@ -143,18 +170,44 @@ export default function NumericShowcase() {
title="Numeric - Double Range"
description="Double range slider for selecting a range of values"
code={`
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]} onChange={onChange}>
// For controlled component
const [controlledValues, setControlledValues] = useState<[number, number]>([0, 100]);
<Numeric.Container
mode="doubleRange"
min={0}
max={100}
step={1}
className="space-y-1"
onChange={vals => console.debug('Values changed:', vals)}
>
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]}>
<Numeric.Container
mode="doubleRange"
min={0}
max={100}
step={1}
defaultValues={[30, 70]}
className="space-y-1"
onChange={vals => console.debug('Values changed:', vals)}
>
<Numeric.Label>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]} className="flex flex-row items-center space-x-2" onChange={onChange}>
<Numeric.Label>Inline double slider</Numeric.Label>
<Numeric.Container
mode="doubleRange"
min={0}
max={100}
step={1}
values={controlledValues}
className="flex flex-row items-center space-x-2"
onChange={vals => setControlledValues(vals as [number, number])}
>
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>`}
>
@@ -164,7 +217,6 @@ export default function NumericShowcase() {
min={0}
max={100}
step={1}
values={[30, 70]}
className="space-y-1"
onChange={vals => console.debug('Values changed:', vals)}
>
@@ -177,8 +229,9 @@ export default function NumericShowcase() {
min={0}
max={100}
step={1}
values={[30, 70]}
defaultValues={[30, 70]}
className="space-y-1"
onChange={vals => console.debug('Values changed:', vals)}
>
<Numeric.Label>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
@@ -189,11 +242,11 @@ export default function NumericShowcase() {
min={0}
max={100}
step={1}
values={[30, 70]}
values={controlledValues}
className="flex flex-row items-center space-x-2"
onChange={vals => console.debug('Values changed:', vals)}
onChange={vals => setControlledValues(vals as [number, number])}
>
<Numeric.Label>Inline double slider</Numeric.Label>
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>
</div>
@@ -204,17 +257,35 @@ export default function NumericShowcase() {
title="Numeric - Combined Examples"
description="Different modes and configurations working together"
code={`
<Numeric.Container mode="number" min={0} max={10} step={0.1}>
<Numeric.Container
mode="number"
min={0}
max={10}
step={0.1}
className="space-y-1"
>
<Numeric.Label>Zoom Factor</Numeric.Label>
<Numeric.NumberInput />
</Numeric.Container>
<Numeric.Container mode="singleRange" min={0} max={360} step={1}>
<Numeric.Container
mode="singleRange"
min={0}
max={360}
step={1}
className="space-y-1"
>
<Numeric.Label showValue>Rotation</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
<Numeric.Container mode="doubleRange" min={-1000} max={3000} values={[1000, 2000]} step={10}>
<Numeric.Container
mode="doubleRange"
min={-1000}
max={3000}
step={10}
className="space-y-1"
>
<Numeric.Label showValue>CT Window</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>`}
@@ -246,7 +317,6 @@ export default function NumericShowcase() {
mode="doubleRange"
min={-1000}
max={3000}
values={[1000, 2000]}
step={10}
className="space-y-1"
>
@@ -1,5 +1,6 @@
// Numeric.tsx
import React, { createContext, useContext, useState, useCallback, PropsWithChildren } from 'react';
import React, { createContext, useContext, useCallback, PropsWithChildren } from 'react';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { cn } from '../../lib/utils';
import { Input } from '../Input/Input';
import { Slider } from '../Slider/Slider';
@@ -23,8 +24,10 @@ const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
---------------------------------------------------------------------------*/
interface NumericMetaContainerProps {
mode: 'number' | 'singleRange' | 'doubleRange';
value?: number; // for single-value usage
values?: [number, number]; // for double-range usage
value?: number; // for controlled single-value usage from parent
defaultValue?: number; // for uncontrolled single-value usage
values?: [number, number]; // for controlled double-range usage from parent
defaultValues?: [number, number]; // for uncontrolled double-range usage
onChange?: (val: number | [number, number]) => void;
min?: number;
max?: number;
@@ -34,8 +37,10 @@ interface NumericMetaContainerProps {
function NumericMetaContainer({
mode,
value = 0,
values = [0, 100],
value,
defaultValue,
values,
defaultValues,
onChange,
min = 0,
max = 100,
@@ -43,26 +48,46 @@ function NumericMetaContainer({
className,
children,
}: PropsWithChildren<NumericMetaContainerProps>) {
// Initialize state with props but don't update automatically
const [internalSingleValue, setInternalSingleValue] = useState<number>(value);
const [internalDoubleValue, setInternalDoubleValue] = useState<[number, number]>(values);
// Calculate default values based on min and max
const calculatedDefaultValue = defaultValue ?? min + (max - min) / 2;
const calculatedDefaultValues = defaultValues ?? [
min + (max - min) * 0.3,
min + (max - min) * 0.7,
];
// Use useControllableState for both single and double values
const [internalSingleValue, setInternalSingleValue] = useControllableState({
prop: mode === 'number' || mode === 'singleRange' ? value : undefined,
defaultProp: calculatedDefaultValue,
onChange: newVal => {
if (mode === 'number' || mode === 'singleRange') {
onChange?.(newVal);
}
},
});
const [internalDoubleValue, setInternalDoubleValue] = useControllableState({
prop: mode === 'doubleRange' ? values : undefined,
defaultProp: calculatedDefaultValues,
onChange: newVals => {
if (mode === 'doubleRange') {
onChange?.(newVals);
}
},
});
const handleSingleChange = useCallback(
(newVal: number) => {
setInternalSingleValue(newVal);
onChange?.(newVal);
},
[onChange]
[setInternalSingleValue]
);
const handleDoubleChange = useCallback(
(newVals: [number, number]) => {
// Update internal state
setInternalDoubleValue(newVals);
// Notify parent if onChange is provided
onChange?.(newVals);
},
[onChange]
[setInternalDoubleValue]
);
return (
+2
View File
@@ -99,6 +99,7 @@ import {
ToolButtonListItem,
ToolButtonListDivider,
Toolbox,
Numeric,
} from './components';
import { DataRow } from './components/DataRow';
@@ -223,4 +224,5 @@ export {
Toolbox,
useToolbox,
utils,
Numeric,
};
@@ -19,8 +19,8 @@ export type CinePlayerProps = {
onClose: () => void;
updateDynamicInfo?: () => void;
dynamicInfo?: {
timePointIndex: number;
numTimePoints: number;
dimensionGroupNumber: number;
numDimensionGroups: number;
label?: string;
};
};
@@ -41,7 +41,7 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
dynamicInfo = {},
updateDynamicInfo,
}) => {
const isDynamic = !!dynamicInfo?.numTimePoints;
const isDynamic = !!dynamicInfo?.numDimensionGroups;
const [frameRate, setFrameRate] = useState(defaultFrameRate);
const debouncedSetFrameRate = useCallback(debounce(onFrameRateChange, 100), [onFrameRateChange]);
@@ -59,14 +59,14 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
setFrameRate(defaultFrameRate);
}, [defaultFrameRate]);
const handleTimePointChange = useCallback(
(newIndex: number) => {
const handleDimensionGroupNumberChange = useCallback(
(newGroupNumber: number) => {
if (isDynamic && dynamicInfo) {
// Here, you would update the component's state or context that controls the current time point index
// For demonstration, assuming a hypothetical function that updates the time point index
updateDynamicInfo({
...dynamicInfo,
timePointIndex: newIndex,
dimensionGroupNumber: newGroupNumber,
});
}
},
@@ -77,10 +77,10 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
<div className={className}>
{isDynamic && dynamicInfo && (
<InputRange
value={dynamicInfo.timePointIndex}
onChange={handleTimePointChange}
minValue={0}
maxValue={dynamicInfo.numTimePoints - 1}
value={dynamicInfo.dimensionGroupNumber}
onChange={handleDimensionGroupNumberChange}
minValue={1}
maxValue={dynamicInfo.numDimensionGroups}
step={1}
containerClassName="mb-3 w-full"
labelClassName="text-xs text-white"
@@ -107,8 +107,8 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
<div className="min-w-16 max-w-44 flex flex-col text-white">
{/* Add Tailwind classes for monospace font and center alignment */}
<div className="text-[11px]">
<span className="w-2 text-white">{dynamicInfo.timePointIndex}</span>{' '}
<span className="text-aqua-pale">{`/${dynamicInfo.numTimePoints}`}</span>
<span className="w-2 text-white">{dynamicInfo.dimensionGroupNumber}</span>{' '}
<span className="text-aqua-pale">{`/${dynamicInfo.numDimensionGroups}`}</span>
</div>
<div className="text-aqua-pale text-xs">{dynamicInfo.label}</div>
</div>
@@ -180,8 +180,8 @@ CinePlayer.propTypes = {
onClose: PropTypes.func,
isDynamic: PropTypes.bool,
dynamicInfo: PropTypes.shape({
timePointIndex: PropTypes.number,
numTimePoints: PropTypes.number,
dimensionGroupNumber: PropTypes.number,
numDimensionGroups: PropTypes.number,
label: PropTypes.string,
}),
};
@@ -1,9 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Types } from '@ohif/core';
import MeasurementItem from './MeasurementItem';
const MeasurementTable = ({
data = [],
@@ -17,9 +14,9 @@ const MeasurementTable = ({
const { t } = useTranslation('MeasurementTable');
const amount = data.length;
const itemCustomization = customizationService.getCustomization('MeasurementItem');
const CustomMeasurementItem = itemCustomization.content;
const CustomMeasurementItem = customizationService.getCustomization(
'microscopyPanel.measurementItem'
);
const onMeasurementDeleteHandler = ({ uid }) => {
const measurement = measurementService.getMeasurement(uid);