Skip to content

Commit

Permalink
feat(VolumeRepresentation): initial add
Browse files Browse the repository at this point in the history
  • Loading branch information
floryst committed Jan 16, 2024
1 parent 23801ba commit f68e400
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"react/jsx-handler-names": 0,
"react/jsx-fragments": 0,
"react/no-unused-prop-types": 0,
"import/export": 0
"import/export": 0,
"n/no-callback-literal": 0,
"@typescript-eslint/no-explicit-any": 0
}
}
233 changes: 233 additions & 0 deletions src/core/VolumeRepresentation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkVolume, {
IVolumeInitialValues,
} from '@kitware/vtk.js/Rendering/Core/Volume';
import vtkVolumeMapper, {
IVolumeMapperInitialValues,
} from '@kitware/vtk.js/Rendering/Core/VolumeMapper';
import { IVolumePropertyInitialValues } from '@kitware/vtk.js/Rendering/Core/VolumeProperty';
import { Vector2 } from '@kitware/vtk.js/types';
import {
forwardRef,
PropsWithChildren,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { IDownstream, IRepresentation } from '../types';
import { compareShallowObject } from '../utils/comparators';
import useBooleanAccumulator from '../utils/useBooleanAccumulator';
import useComparableEffect from '../utils/useComparableEffect';
import useLatest from '../utils/useLatest';
import {
DownstreamContext,
RepresentationContext,
useRendererContext,
} from './contexts';
import useColorTransferFunction from './modules/useColorTransferFunction';
import useDataRange from './modules/useDataRange';
import useMapper from './modules/useMapper';
import usePiecewiseFunction from './modules/usePiecewiseFunction';
import useProp from './modules/useProp';

export interface VolumeRepresentationProps extends PropsWithChildren {
/**
* The ID used to identify this component.
*/
id?: string;

/**
* Properties to set to the mapper
*/
mapper?: IVolumeMapperInitialValues;

/**
* An opational mapper instanc
*/
mapperInstance?: vtkVolumeMapper;

/**
* Properties to set to the volume actor
*/
actor?: IVolumeInitialValues;

/**
* Properties to set to the volume.property
*/
property?: IVolumePropertyInitialValues;

/**
* Preset name for the lookup table color map
*/
colorMapPreset?: string;

/**
* Data range use for the colorMap
*/
colorDataRange?: 'auto' | Vector2;

/**
* Event callback for when data is made available.
*
* By the time this callback is invoked, you can be sure that:
* - the mapper has the input data
* - the actor is visible (unless explicitly marked as not visible)
* - initial properties are set
*/
onDataAvailable?: () => void;
}

const DefaultProps = {
colorMapPreset: 'erdc_rainbow_bright',
colorDataRange: 'auto' as const,
};

export default forwardRef(function VolumeRepresentation(
props: VolumeRepresentationProps,
fwdRef
) {
const [modifiedRef, trackModified, resetModified] = useBooleanAccumulator();
const [dataAvailable, setDataAvailable] = useState(false);

// --- mapper --- //

const getInternalMapper = useMapper(
() => vtkVolumeMapper.newInstance(),
props.mapper,
trackModified
);

const { mapperInstance } = props;
const getMapper = useCallback(() => {
if (mapperInstance) {
return mapperInstance;
}
return getInternalMapper();
}, [mapperInstance, getInternalMapper]);

// --- data range --- //

const getDataArray = useCallback(
() =>
getMapper()?.getInputData()?.getPointData().getScalars() as
| vtkDataArray
| undefined,
[getMapper]
);

const { dataRange, updateDataRange } = useDataRange(getDataArray);

const rangeFromProps = props.colorDataRange ?? DefaultProps.colorDataRange;
const colorDataRange = rangeFromProps === 'auto' ? dataRange : rangeFromProps;

// --- LUT --- //

const getLookupTable = useColorTransferFunction(
props.colorMapPreset ?? DefaultProps.colorMapPreset,
colorDataRange,
trackModified
);

// --- PWF --- //

const getPiecewiseFunction = usePiecewiseFunction(
colorDataRange,
trackModified
);

// --- actor --- //

const actorProps = {
...props.actor,
visibility: dataAvailable && (props.actor?.visibility ?? true),
};
const getActor = useProp({
constructor: () => vtkVolume.newInstance(),
id: props.id,
props: actorProps,
trackModified,
});

useEffect(() => {
getActor().setMapper(getMapper());
}, [getActor, getMapper]);

useEffect(() => {
getActor().getProperty().setRGBTransferFunction(0, getLookupTable());
getActor().getProperty().setScalarOpacity(0, getPiecewiseFunction());
getActor().getProperty().setInterpolationTypeToLinear();
}, [getActor, getLookupTable, getPiecewiseFunction]);

// set actor property props
const { property: propertyProps } = props;
useComparableEffect(
() => {
if (!propertyProps) return;
trackModified(getActor().getProperty().set(propertyProps));
},
[propertyProps],
([cur], [prev]) => compareShallowObject(cur, prev)
);

// --- events --- //

const onDataAvailable = useLatest(props.onDataAvailable);
useEffect(() => {
if (dataAvailable) {
// trigger onDataAvailable after making updates to the actor and mapper
onDataAvailable.current?.();
}
// onDataAvailable is a ref
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataAvailable]);

// --- //

const renderer = useRendererContext();

useEffect(() => {
if (modifiedRef.current) {
renderer.requestRender();
resetModified();
}
});

const representation = useMemo<IRepresentation>(
() => ({
dataChanged: () => {
updateDataRange();
renderer.requestRender();
},
dataAvailable: (available = true) => {
setDataAvailable(available);
representation.dataChanged();
},
getActor,
getMapper,
getData: () => {
return getMapper().getInputData();
},
}),
[renderer, updateDataRange, getActor, getMapper]
);

const downstream = useMemo<IDownstream>(
() => ({
setInputData: (...args) => getMapper().setInputData(...args),
setInputConnection: (...args) => getMapper().setInputConnection(...args),
}),
[getMapper]
);

useImperativeHandle(fwdRef, () => representation);

return (
<RepresentationContext.Provider value={representation}>
<DownstreamContext.Provider value={downstream}>
{props.children}
</DownstreamContext.Provider>
</RepresentationContext.Provider>
);
});
26 changes: 26 additions & 0 deletions src/core/modules/useDataRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import { useCallback, useEffect, useState } from 'react';

const DEFAULT_RANGE: [number, number] = [0, 1];

export default function useDataRange(
getDataArray: () => vtkDataArray | null | undefined,
defaultRange = DEFAULT_RANGE
) {
const [dataRange, setRange] = useState<[number, number]>(defaultRange);

const updateDataRange = useCallback(() => {
const range = getDataArray()?.getRange();
if (!range) return;
setRange(range);
}, [getDataArray]);

useEffect(() => {
updateDataRange();
}, [updateDataRange]);

return {
dataRange,
updateDataRange,
};
}
42 changes: 42 additions & 0 deletions src/core/modules/usePiecewiseFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
import { Vector2 } from '@kitware/vtk.js/types';
import { compareVector2 } from '../../utils/comparators';
import deletionRegistry from '../../utils/DeletionRegistry';
import { BooleanAccumulator } from '../../utils/useBooleanAccumulator';
import useComparableEffect from '../../utils/useComparableEffect';
import useGetterRef from '../../utils/useGetterRef';
import useUnmount from '../../utils/useUnmount';

export default function usePiecewiseFunction(
range: Vector2,
trackModified: BooleanAccumulator
) {
const [pwfRef, getPWF] = useGetterRef(() => {
const func = vtkPiecewiseFunction.newInstance();
deletionRegistry.register(func, () => func.delete());
return func;
});

useComparableEffect(
() => {
if (!range) return;
const pwf = getPWF();
pwf.setNodes([
{ x: range[0], y: 0, midpoint: 0.5, sharpness: 0 },
{ x: range[1], y: 1, midpoint: 0.5, sharpness: 0 },
]);
trackModified(true);
},
[range] as const,
([curRange], [oldRange]) => compareVector2(curRange, oldRange)
);

useUnmount(() => {
if (pwfRef.current) {
deletionRegistry.markForDeletion(pwfRef.current);
pwfRef.current = null;
}
});

return getPWF;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ export { default as SliceRepresentation } from './core/SliceRepresentation';
export type { SliceRepresentationProps } from './core/SliceRepresentation';
export { default as View } from './core/View';
export type { ViewProps } from './core/View';
export { default as VolumeRepresentation } from './core/VolumeRepresentation';

0 comments on commit f68e400

Please sign in to comment.