Skip to content

Commit f68e400

Browse files
committed
feat(VolumeRepresentation): initial add
1 parent 23801ba commit f68e400

File tree

5 files changed

+305
-1
lines changed

5 files changed

+305
-1
lines changed

.eslintrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"react/jsx-handler-names": 0,
3333
"react/jsx-fragments": 0,
3434
"react/no-unused-prop-types": 0,
35-
"import/export": 0
35+
"import/export": 0,
36+
"n/no-callback-literal": 0,
37+
"@typescript-eslint/no-explicit-any": 0
3638
}
3739
}

src/core/VolumeRepresentation.tsx

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
2+
import vtkVolume, {
3+
IVolumeInitialValues,
4+
} from '@kitware/vtk.js/Rendering/Core/Volume';
5+
import vtkVolumeMapper, {
6+
IVolumeMapperInitialValues,
7+
} from '@kitware/vtk.js/Rendering/Core/VolumeMapper';
8+
import { IVolumePropertyInitialValues } from '@kitware/vtk.js/Rendering/Core/VolumeProperty';
9+
import { Vector2 } from '@kitware/vtk.js/types';
10+
import {
11+
forwardRef,
12+
PropsWithChildren,
13+
useCallback,
14+
useEffect,
15+
useImperativeHandle,
16+
useMemo,
17+
useState,
18+
} from 'react';
19+
import { IDownstream, IRepresentation } from '../types';
20+
import { compareShallowObject } from '../utils/comparators';
21+
import useBooleanAccumulator from '../utils/useBooleanAccumulator';
22+
import useComparableEffect from '../utils/useComparableEffect';
23+
import useLatest from '../utils/useLatest';
24+
import {
25+
DownstreamContext,
26+
RepresentationContext,
27+
useRendererContext,
28+
} from './contexts';
29+
import useColorTransferFunction from './modules/useColorTransferFunction';
30+
import useDataRange from './modules/useDataRange';
31+
import useMapper from './modules/useMapper';
32+
import usePiecewiseFunction from './modules/usePiecewiseFunction';
33+
import useProp from './modules/useProp';
34+
35+
export interface VolumeRepresentationProps extends PropsWithChildren {
36+
/**
37+
* The ID used to identify this component.
38+
*/
39+
id?: string;
40+
41+
/**
42+
* Properties to set to the mapper
43+
*/
44+
mapper?: IVolumeMapperInitialValues;
45+
46+
/**
47+
* An opational mapper instanc
48+
*/
49+
mapperInstance?: vtkVolumeMapper;
50+
51+
/**
52+
* Properties to set to the volume actor
53+
*/
54+
actor?: IVolumeInitialValues;
55+
56+
/**
57+
* Properties to set to the volume.property
58+
*/
59+
property?: IVolumePropertyInitialValues;
60+
61+
/**
62+
* Preset name for the lookup table color map
63+
*/
64+
colorMapPreset?: string;
65+
66+
/**
67+
* Data range use for the colorMap
68+
*/
69+
colorDataRange?: 'auto' | Vector2;
70+
71+
/**
72+
* Event callback for when data is made available.
73+
*
74+
* By the time this callback is invoked, you can be sure that:
75+
* - the mapper has the input data
76+
* - the actor is visible (unless explicitly marked as not visible)
77+
* - initial properties are set
78+
*/
79+
onDataAvailable?: () => void;
80+
}
81+
82+
const DefaultProps = {
83+
colorMapPreset: 'erdc_rainbow_bright',
84+
colorDataRange: 'auto' as const,
85+
};
86+
87+
export default forwardRef(function VolumeRepresentation(
88+
props: VolumeRepresentationProps,
89+
fwdRef
90+
) {
91+
const [modifiedRef, trackModified, resetModified] = useBooleanAccumulator();
92+
const [dataAvailable, setDataAvailable] = useState(false);
93+
94+
// --- mapper --- //
95+
96+
const getInternalMapper = useMapper(
97+
() => vtkVolumeMapper.newInstance(),
98+
props.mapper,
99+
trackModified
100+
);
101+
102+
const { mapperInstance } = props;
103+
const getMapper = useCallback(() => {
104+
if (mapperInstance) {
105+
return mapperInstance;
106+
}
107+
return getInternalMapper();
108+
}, [mapperInstance, getInternalMapper]);
109+
110+
// --- data range --- //
111+
112+
const getDataArray = useCallback(
113+
() =>
114+
getMapper()?.getInputData()?.getPointData().getScalars() as
115+
| vtkDataArray
116+
| undefined,
117+
[getMapper]
118+
);
119+
120+
const { dataRange, updateDataRange } = useDataRange(getDataArray);
121+
122+
const rangeFromProps = props.colorDataRange ?? DefaultProps.colorDataRange;
123+
const colorDataRange = rangeFromProps === 'auto' ? dataRange : rangeFromProps;
124+
125+
// --- LUT --- //
126+
127+
const getLookupTable = useColorTransferFunction(
128+
props.colorMapPreset ?? DefaultProps.colorMapPreset,
129+
colorDataRange,
130+
trackModified
131+
);
132+
133+
// --- PWF --- //
134+
135+
const getPiecewiseFunction = usePiecewiseFunction(
136+
colorDataRange,
137+
trackModified
138+
);
139+
140+
// --- actor --- //
141+
142+
const actorProps = {
143+
...props.actor,
144+
visibility: dataAvailable && (props.actor?.visibility ?? true),
145+
};
146+
const getActor = useProp({
147+
constructor: () => vtkVolume.newInstance(),
148+
id: props.id,
149+
props: actorProps,
150+
trackModified,
151+
});
152+
153+
useEffect(() => {
154+
getActor().setMapper(getMapper());
155+
}, [getActor, getMapper]);
156+
157+
useEffect(() => {
158+
getActor().getProperty().setRGBTransferFunction(0, getLookupTable());
159+
getActor().getProperty().setScalarOpacity(0, getPiecewiseFunction());
160+
getActor().getProperty().setInterpolationTypeToLinear();
161+
}, [getActor, getLookupTable, getPiecewiseFunction]);
162+
163+
// set actor property props
164+
const { property: propertyProps } = props;
165+
useComparableEffect(
166+
() => {
167+
if (!propertyProps) return;
168+
trackModified(getActor().getProperty().set(propertyProps));
169+
},
170+
[propertyProps],
171+
([cur], [prev]) => compareShallowObject(cur, prev)
172+
);
173+
174+
// --- events --- //
175+
176+
const onDataAvailable = useLatest(props.onDataAvailable);
177+
useEffect(() => {
178+
if (dataAvailable) {
179+
// trigger onDataAvailable after making updates to the actor and mapper
180+
onDataAvailable.current?.();
181+
}
182+
// onDataAvailable is a ref
183+
// eslint-disable-next-line react-hooks/exhaustive-deps
184+
}, [dataAvailable]);
185+
186+
// --- //
187+
188+
const renderer = useRendererContext();
189+
190+
useEffect(() => {
191+
if (modifiedRef.current) {
192+
renderer.requestRender();
193+
resetModified();
194+
}
195+
});
196+
197+
const representation = useMemo<IRepresentation>(
198+
() => ({
199+
dataChanged: () => {
200+
updateDataRange();
201+
renderer.requestRender();
202+
},
203+
dataAvailable: (available = true) => {
204+
setDataAvailable(available);
205+
representation.dataChanged();
206+
},
207+
getActor,
208+
getMapper,
209+
getData: () => {
210+
return getMapper().getInputData();
211+
},
212+
}),
213+
[renderer, updateDataRange, getActor, getMapper]
214+
);
215+
216+
const downstream = useMemo<IDownstream>(
217+
() => ({
218+
setInputData: (...args) => getMapper().setInputData(...args),
219+
setInputConnection: (...args) => getMapper().setInputConnection(...args),
220+
}),
221+
[getMapper]
222+
);
223+
224+
useImperativeHandle(fwdRef, () => representation);
225+
226+
return (
227+
<RepresentationContext.Provider value={representation}>
228+
<DownstreamContext.Provider value={downstream}>
229+
{props.children}
230+
</DownstreamContext.Provider>
231+
</RepresentationContext.Provider>
232+
);
233+
});

src/core/modules/useDataRange.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
2+
import { useCallback, useEffect, useState } from 'react';
3+
4+
const DEFAULT_RANGE: [number, number] = [0, 1];
5+
6+
export default function useDataRange(
7+
getDataArray: () => vtkDataArray | null | undefined,
8+
defaultRange = DEFAULT_RANGE
9+
) {
10+
const [dataRange, setRange] = useState<[number, number]>(defaultRange);
11+
12+
const updateDataRange = useCallback(() => {
13+
const range = getDataArray()?.getRange();
14+
if (!range) return;
15+
setRange(range);
16+
}, [getDataArray]);
17+
18+
useEffect(() => {
19+
updateDataRange();
20+
}, [updateDataRange]);
21+
22+
return {
23+
dataRange,
24+
updateDataRange,
25+
};
26+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
2+
import { Vector2 } from '@kitware/vtk.js/types';
3+
import { compareVector2 } from '../../utils/comparators';
4+
import deletionRegistry from '../../utils/DeletionRegistry';
5+
import { BooleanAccumulator } from '../../utils/useBooleanAccumulator';
6+
import useComparableEffect from '../../utils/useComparableEffect';
7+
import useGetterRef from '../../utils/useGetterRef';
8+
import useUnmount from '../../utils/useUnmount';
9+
10+
export default function usePiecewiseFunction(
11+
range: Vector2,
12+
trackModified: BooleanAccumulator
13+
) {
14+
const [pwfRef, getPWF] = useGetterRef(() => {
15+
const func = vtkPiecewiseFunction.newInstance();
16+
deletionRegistry.register(func, () => func.delete());
17+
return func;
18+
});
19+
20+
useComparableEffect(
21+
() => {
22+
if (!range) return;
23+
const pwf = getPWF();
24+
pwf.setNodes([
25+
{ x: range[0], y: 0, midpoint: 0.5, sharpness: 0 },
26+
{ x: range[1], y: 1, midpoint: 0.5, sharpness: 0 },
27+
]);
28+
trackModified(true);
29+
},
30+
[range] as const,
31+
([curRange], [oldRange]) => compareVector2(curRange, oldRange)
32+
);
33+
34+
useUnmount(() => {
35+
if (pwfRef.current) {
36+
deletionRegistry.markForDeletion(pwfRef.current);
37+
pwfRef.current = null;
38+
}
39+
});
40+
41+
return getPWF;
42+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ export { default as SliceRepresentation } from './core/SliceRepresentation';
4444
export type { SliceRepresentationProps } from './core/SliceRepresentation';
4545
export { default as View } from './core/View';
4646
export type { ViewProps } from './core/View';
47+
export { default as VolumeRepresentation } from './core/VolumeRepresentation';

0 commit comments

Comments
 (0)