Skip to content

Commit ff17c54

Browse files
committed
feat: update for vtkImageArrayMapper
Update SliceRepresentation to allow use of the new vtkImageArrayMapper. Add a new Dataset component to take a callback as property for loading input data with async function. Add example `ImageSeriesRendering` to demo the vtkImageArrayMapper.
1 parent 5182206 commit ff17c54

8 files changed

Lines changed: 476 additions & 34 deletions

File tree

src/core/Dataset.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import {
5+
RepresentationContext,
6+
DownstreamContext,
7+
DataSetContext,
8+
} from './View';
9+
10+
/**
11+
* The Dataset component exposes any input data object to a downstream filter.
12+
* It allows you to set the input data instance directly through props.data,
13+
* or through the async/lazy loading mechanism using a callback function
14+
* props.fetchData().
15+
* It takes the following set of properties:
16+
* - data: directly sets the input data instance.
17+
* - fetchData: callback function to fetch input data asynchronously.
18+
*/
19+
export default class Dataset extends Component {
20+
componentDidMount() {
21+
if (this.dataset && !this.dataset.isDeleted()) {
22+
// data already available
23+
this.dataAvailable();
24+
} else {
25+
// update data from current props
26+
const prevProps = { data: null, fetchData: null };
27+
this.update(this.props, prevProps);
28+
}
29+
}
30+
31+
componentWillUnmount() {}
32+
33+
render() {
34+
return (
35+
<RepresentationContext.Consumer>
36+
{(representation) => (
37+
<DownstreamContext.Consumer>
38+
{(downstream) => {
39+
this.representation = representation;
40+
if (!this.downstream) {
41+
this.downstream = downstream;
42+
}
43+
return (
44+
<DataSetContext.Provider value={this}>
45+
<div key={this.props.id} id={this.props.id}>
46+
{this.props.children}
47+
</div>
48+
</DataSetContext.Provider>
49+
);
50+
}}
51+
</DownstreamContext.Consumer>
52+
)}
53+
</RepresentationContext.Consumer>
54+
);
55+
}
56+
57+
componentDidUpdate(prevProps, prevState, snapshot) {
58+
this.update(this.props, prevProps);
59+
}
60+
61+
update(props, previous) {
62+
const { data, fetchData } = props;
63+
if (data && data !== previous.data) {
64+
// direct assignment of data object
65+
this.dataset = data;
66+
this.dataAvailable();
67+
} else if (fetchData && fetchData !== previous.fetchData) {
68+
// async fetch data
69+
fetchData().then((response) => {
70+
if (response) {
71+
this.dataset = response;
72+
this.dataAvailable();
73+
}
74+
});
75+
}
76+
}
77+
78+
dataAvailable() {
79+
if (this.downstream && this.dataset) {
80+
this.downstream.setInputData(this.dataset);
81+
}
82+
83+
if (this.representation) {
84+
this.representation.dataAvailable();
85+
this.representation.dataChanged();
86+
}
87+
}
88+
}
89+
90+
Dataset.defaultProps = {
91+
data: null,
92+
fetchData: null,
93+
};
94+
95+
Dataset.propTypes = {
96+
/**
97+
* The ID used to identify this component.
98+
*/
99+
id: PropTypes.string,
100+
101+
/**
102+
* Directly set the dataset object as a property value.
103+
*/
104+
data: PropTypes.object,
105+
106+
/**
107+
* Optional callback function for async loading of input data.
108+
*/
109+
fetchData: PropTypes.func,
110+
111+
children: PropTypes.oneOfType([
112+
PropTypes.arrayOf(PropTypes.node),
113+
PropTypes.node,
114+
]),
115+
};

src/core/SliceRepresentation.js

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export default class SliceRepresentation extends Component {
3333
this.lookupTable.applyColorMap(preset);
3434
this.piecewiseFunction = vtkPiecewiseFunction.newInstance();
3535
this.actor = vtkImageSlice.newInstance({ visibility: false });
36-
this.mapper = vtkImageMapper.newInstance();
36+
// use the mapper instance if provided, otherwise create default instance.
37+
this.mapper = props.mapperInstance ?? vtkImageMapper.newInstance();
3738
this.actor.setMapper(this.mapper);
38-
3939
this.actor.getProperty().setRGBTransferFunction(0, this.lookupTable);
4040
// this.actor.getProperty().setScalarOpacity(0, this.piecewiseFunction);
4141
this.actor.getProperty().setInterpolationTypeToLinear();
@@ -105,7 +105,11 @@ export default class SliceRepresentation extends Component {
105105
if (property && (!previous || property !== previous.property)) {
106106
changed = this.actor.getProperty().set(property) || changed;
107107
}
108-
if (mapper && (!previous || mapper !== previous.mapper)) {
108+
if (
109+
mapper &&
110+
(!previous || mapper !== previous.mapper) &&
111+
mapper !== this.mapper
112+
) {
109113
changed = this.mapper.set(mapper) || changed;
110114
}
111115
if (
@@ -146,25 +150,35 @@ export default class SliceRepresentation extends Component {
146150
}
147151
}
148152

149-
// ijk
150-
if (iSlice != null && (!previous || iSlice !== previous.iSlice)) {
151-
changed = this.mapper.setISlice(iSlice) || changed;
152-
}
153-
if (jSlice != null && (!previous || jSlice !== previous.jSlice)) {
154-
changed = this.mapper.setJSlice(jSlice) || changed;
155-
}
156-
if (kSlice != null && (!previous || kSlice !== previous.kSlice)) {
157-
changed = this.mapper.setKSlice(kSlice) || changed;
158-
}
159-
// xyz
160-
if (xSlice != null && (!previous || xSlice !== previous.xSlice)) {
161-
changed = this.mapper.setXSlice(xSlice) || changed;
162-
}
163-
if (ySlice != null && (!previous || ySlice !== previous.ySlice)) {
164-
changed = this.mapper.setYSlice(ySlice) || changed;
165-
}
166-
if (zSlice != null && (!previous || zSlice !== previous.zSlice)) {
167-
changed = this.mapper.setZSlice(zSlice) || changed;
153+
// check if we have valid input
154+
if (this.validData) {
155+
if (this.mapper.isA('vtkImageMapper')) {
156+
// ijk
157+
if (iSlice != null && (!previous || iSlice !== previous.iSlice)) {
158+
changed = this.mapper.setISlice(iSlice) || changed;
159+
}
160+
if (jSlice != null && (!previous || jSlice !== previous.jSlice)) {
161+
changed = this.mapper.setJSlice(jSlice) || changed;
162+
}
163+
if (kSlice != null && (!previous || kSlice !== previous.kSlice)) {
164+
changed = this.mapper.setKSlice(kSlice) || changed;
165+
}
166+
// xyz
167+
if (xSlice != null && (!previous || xSlice !== previous.xSlice)) {
168+
changed = this.mapper.setXSlice(xSlice) || changed;
169+
}
170+
if (ySlice != null && (!previous || ySlice !== previous.ySlice)) {
171+
changed = this.mapper.setYSlice(ySlice) || changed;
172+
}
173+
if (zSlice != null && (!previous || zSlice !== previous.zSlice)) {
174+
changed = this.mapper.setZSlice(zSlice) || changed;
175+
}
176+
} else if (this.mapper.isA('vtkImageArrayMapper')) {
177+
// vtkImageArrayMapper only supports k-slicing
178+
if (kSlice != null && (!previous || kSlice !== previous.kSlice)) {
179+
changed = this.mapper.setSlice(kSlice) || changed;
180+
}
181+
}
168182
}
169183

170184
// actor visibility
@@ -186,6 +200,11 @@ export default class SliceRepresentation extends Component {
186200
this.validData = true;
187201
this.actor.setVisibility(this.currentVisibility);
188202

203+
// reset camera after input data is lazy-loaded
204+
if (this.view && this.view.props.autoResetCamera) {
205+
this.view.resetCamera();
206+
}
207+
189208
// trigger render
190209
this.dataChanged();
191210
}
@@ -194,16 +213,18 @@ export default class SliceRepresentation extends Component {
194213
dataChanged() {
195214
if (this.props.colorDataRange === 'auto') {
196215
this.mapper.update();
197-
const input = this.mapper.getInputData();
198-
const array = input && input.getPointData()?.getScalars();
199-
const dataRange = array && array.getRange();
200-
if (dataRange) {
201-
this.lookupTable.setMappingRange(...dataRange);
202-
this.lookupTable.updateRange();
203-
this.piecewiseFunction.setNodes([
204-
{ x: dataRange[0], y: 0, midpoint: 0.5, sharpness: 0 },
205-
{ x: dataRange[1], y: 1, midpoint: 0.5, sharpness: 0 },
206-
]);
216+
if (this.mapper.getInputData()) {
217+
const input = this.mapper.getCurrentImage();
218+
const array = input && input.getPointData()?.getScalars();
219+
const dataRange = array && array.getRange();
220+
if (dataRange) {
221+
this.lookupTable.setMappingRange(...dataRange);
222+
this.lookupTable.updateRange();
223+
this.piecewiseFunction.setNodes([
224+
{ x: dataRange[0], y: 0, midpoint: 0.5, sharpness: 0 },
225+
{ x: dataRange[1], y: 1, midpoint: 0.5, sharpness: 0 },
226+
]);
227+
}
207228
}
208229

209230
if (this.view) {
@@ -229,6 +250,12 @@ SliceRepresentation.propTypes = {
229250
*/
230251
mapper: PropTypes.object,
231252

253+
/**
254+
* Optional parameter to set vtk mapper instance from outside.
255+
* Allows to control which mapper class {vtkImageMapper, vtkImageArrayMapper} to use.
256+
*/
257+
mapperInstance: PropTypes.object,
258+
232259
/**
233260
* Properties to set to the slice/actor
234261
*/

src/core/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import vtkPointData from './PointData';
55
import vtkPolyData from './PolyData';
66
import vtkReader from './Reader';
77
import vtkShareDataSet from './ShareDataSet';
8+
import vtkDataset from './Dataset';
89
import vtkView from './ViewContainer';
910
import vtkGeometryRepresentation from './GeometryRepresentation';
1011
import vtkGeometry2DRepresentation from './Geometry2DRepresentation';
@@ -24,6 +25,7 @@ export const PointData = vtkPointData;
2425
export const PolyData = vtkPolyData;
2526
export const Reader = vtkReader;
2627
export const ShareDataSet = vtkShareDataSet;
28+
export const DataSet = vtkDataset;
2729
export const View = vtkView;
2830
export const GeometryRepresentation = vtkGeometryRepresentation;
2931
export const Geometry2DRepresentation = vtkGeometry2DRepresentation;
@@ -44,6 +46,7 @@ export default {
4446
PolyData: vtkPolyData,
4547
Reader: vtkReader,
4648
ShareDataSet: vtkShareDataSet,
49+
Dataset: vtkDataset,
4750
View: vtkView,
4851
GeometryRepresentation: vtkGeometryRepresentation,
4952
Geometry2DRepresentation: vtkGeometry2DRepresentation,

src/light.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const PointData = Core.PointData;
1818
export const PolyData = Core.PolyData;
1919
export const Reader = Core.Reader;
2020
export const ShareDataSet = Core.ShareDataSet;
21+
export const Dataset = Core.Dataset;
2122
export const View = Core.View;
2223
export const GeometryRepresentation = Core.GeometryRepresentation;
2324
export const Geometry2DRepresentation = Core.Geometry2DRepresentation;

usage/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

usage/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"react-vtk-js": "file:..",
13+
"fflate": "^0.7.4",
1414
"react": "^17.0.2",
15-
"react-dom": "^17.0.2"
15+
"react-dom": "^17.0.2",
16+
"react-vtk-js": "file:.."
1617
},
1718
"devDependencies": {
1819
"@vitejs/plugin-react": "^1.0.7",

usage/src/App.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const SourceViewer = lazy(() => import('./Geometry/SourceViewer'));
1212
const Glyph = lazy(() => import('./Geometry/Glyph'));
1313
const CutterExample = lazy(() => import('./Geometry/CutterExample'));
1414
const SliceRendering = lazy(() => import('./Volume/SliceRendering'));
15+
const ImageSeriesRendering = lazy(() => import('./Volume/ImageSeriesRendering'));
1516
const SyntheticVolumeRendering = lazy(() =>
1617
import('./Volume/SyntheticVolumeRendering')
1718
);
@@ -30,6 +31,7 @@ const demos = [
3031
'Geometry/Glyph',
3132
'Geometry/CutterExample',
3233
'Volume/SliceRendering',
34+
'Volume/ImageSeriesRendering',
3335
'Volume/SyntheticVolumeRendering',
3436
'Volume/VolumeRendering',
3537
'Volume/DynamicUpdate',
@@ -90,6 +92,7 @@ function App() {
9092
{example === 'Geometry/Glyph' && <Glyph />}
9193
{example === 'Geometry/CutterExample' && <CutterExample />}
9294
{example === 'Volume/SliceRendering' && <SliceRendering />}
95+
{example === 'Volume/ImageSeriesRendering' && <ImageSeriesRendering />}
9396
{example === 'Volume/SyntheticVolumeRendering' && (
9497
<SyntheticVolumeRendering />
9598
)}

0 commit comments

Comments
 (0)