diff --git a/package-lock.json b/package-lock.json index 7a2ffc7..fca0bf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@babel/plugin-transform-runtime": "^7.12.10", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", - "@kitware/vtk.js": "^24.3.1", + "@kitware/vtk.js": "^24.7.1", "@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-commonjs": "17.0.0", "@rollup/plugin-eslint": "^8.0.1", @@ -46,7 +46,7 @@ "semantic-release": "17.3.1" }, "peerDependencies": { - "@kitware/vtk.js": "^24.3.1", + "@kitware/vtk.js": "^24.7.1", "react": "^16.0.0" } }, @@ -2128,9 +2128,9 @@ "dev": true }, "node_modules/@kitware/vtk.js": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-24.3.1.tgz", - "integrity": "sha512-SLDjDPohq1esh4MIYBqMQQYGu/Um4x5bbnuLyFo9l0bg1KmgLqC1IGnCtWecG5w0LR62JbSAHCcp/m9Zd4GaAw==", + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-24.7.1.tgz", + "integrity": "sha512-geOFGGpLFertWHu1oo23kScpd4fduBetOWtxFPWf1wlTHw/vxQNnp7+razqBIALOSYjFVftj8zplFAWW58cHhQ==", "dev": true, "dependencies": { "@babel/runtime": "7.16.7", @@ -16905,9 +16905,9 @@ "dev": true }, "@kitware/vtk.js": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-24.3.1.tgz", - "integrity": "sha512-SLDjDPohq1esh4MIYBqMQQYGu/Um4x5bbnuLyFo9l0bg1KmgLqC1IGnCtWecG5w0LR62JbSAHCcp/m9Zd4GaAw==", + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-24.7.1.tgz", + "integrity": "sha512-geOFGGpLFertWHu1oo23kScpd4fduBetOWtxFPWf1wlTHw/vxQNnp7+razqBIALOSYjFVftj8zplFAWW58cHhQ==", "dev": true, "requires": { "@babel/runtime": "7.16.7", diff --git a/package.json b/package.json index c7b69d3..ff2700b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dev": "rollup ./src/index.js -c --watch" }, "peerDependencies": { - "@kitware/vtk.js": "^24.3.1", + "@kitware/vtk.js": "^24.7.1", "react": "^16.0.0" }, "devDependencies": { @@ -36,7 +36,7 @@ "@babel/plugin-transform-runtime": "^7.12.10", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", - "@kitware/vtk.js": "^24.3.1", + "@kitware/vtk.js": "^24.7.1", "@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-commonjs": "17.0.0", "@rollup/plugin-eslint": "^8.0.1", diff --git a/src/core/MultiViewRoot.js b/src/core/MultiViewRoot.js new file mode 100644 index 0000000..c83f7bf --- /dev/null +++ b/src/core/MultiViewRoot.js @@ -0,0 +1,183 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; +import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow'; + +// ---------------------------------------------------------------------------- +// Context to pass parent variables to children +// ---------------------------------------------------------------------------- + +export const MultiViewRootContext = React.createContext(null); + +export function removeKeys(props, propNames) { + const cleanedProps = { ...props }; + propNames.forEach((name) => { + delete cleanedProps[name]; + }); + return cleanedProps; +} + +// ---------------------------------------------------------------------------- +// Helper constants +// ---------------------------------------------------------------------------- + +const RENDERER_STYLE = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + pointerEvents: 'none', +}; + +export default class MultiViewRoot extends Component { + constructor(props) { + super(props); + this.containerRef = React.createRef(); + + // Create vtk.js view + this.renderWindow = vtkRenderWindow.newInstance(); + this.interactor = null; + + this.renderWindowView = vtkOpenGLRenderWindow.newInstance(); + this.renderWindow.addView(this.renderWindowView); + + this.resizeObserver = new ResizeObserver((entries) => { + this.onResize(); + }); + + this.interactor = vtkRenderWindowInteractor.newInstance(); + this.interactor.setView(this.renderWindowView); + + this.onResize = this.onResize.bind(this); + + this.initialized = false; + } + + componentDidMount() { + // TODO support runtime toggling of this flag? + if (!this.props.disabled) { + const container = this.containerRef.current; + this.renderWindowView.setContainer(container); + + this.interactor.initialize(); + + this.resizeObserver.observe(container); + this.onResize(); + + this.initialized = true; + + this.update(this.props); + } + } + + componentDidUpdate(prevProps) { + this.update(this.props, prevProps); + } + + componentWillUnmount() { + if (this.initialized) { + // Stop size listening + this.resizeObserver.disconnect(); + + if (this.interactor.getContainer()) { + this.interactor.unbindEvents(); + } + + this.renderWindowView.setContainer(null); + } + + this.renderWindow.removeView(this.renderWindowView); + + this.interactor.delete(); + this.renderWindow.delete(); + this.renderWindowView.delete(); + + this.interactor = null; + this.renderWindow = null; + this.renderWindowView = null; + } + + render() { + const { id, children, style, disabled } = this.props; + + return ( +
+
+ + {children} + +
+ ); + } + + bindInteractorEvents(container) { + if (this.interactor) { + if (this.interactor.getContainer()) { + this.interactor.unbindEvents(); + } + if (container) { + this.interactor.bindEvents(container); + } + } + } + + onResize() { + const container = this.containerRef.current; + if (container) { + const devicePixelRatio = window.devicePixelRatio || 1; + const { width, height } = container.getBoundingClientRect(); + const w = Math.floor(width * devicePixelRatio); + const h = Math.floor(height * devicePixelRatio); + this.renderWindowView.setSize(Math.max(w, 10), Math.max(h, 10)); + this.renderWindow.render(); + } + } + + update(props, previous) { + const { triggerRender } = props; + // Allow to trigger method call from property change + if (previous && triggerRender !== previous.triggerRender) { + this.renderViewTimeout = setTimeout(this.renderWindow.render, 0); + } + } +} + +MultiViewRoot.defaultProps = { + triggerRender: 0, + disabled: false, +}; + +export const propTypes = { + /** + * The ID used to identify this component. + */ + id: PropTypes.string, + + /** + * Property use to trigger a render when changing. + */ + triggerRender: PropTypes.number, + + /** + * Disables or enables the multi-renderer root. + */ + disabled: PropTypes.bool, + + /** + * List of representation to show + */ + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), +}; + +MultiViewRoot.propTypes = propTypes; diff --git a/src/core/View.js b/src/core/View.js index 8f29ce4..86503d0 100644 --- a/src/core/View.js +++ b/src/core/View.js @@ -7,17 +7,12 @@ import PropTypes from 'prop-types'; import { debounce } from '@kitware/vtk.js/macros.js'; -import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow.js'; -import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow.js'; -import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor.js'; -import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer.js'; -import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator.js'; - import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox.js'; import vtkCubeAxesActor from '@kitware/vtk.js/Rendering/Core/CubeAxesActor.js'; import vtkAxesActor from '@kitware/vtk.js/Rendering/Core/AxesActor'; import vtkOrientationMarkerWidget from '@kitware/vtk.js/Interaction/Widgets/OrientationMarkerWidget'; +import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator.js'; // Style modes import vtkMouseCameraTrackballMultiRotateManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballMultiRotateManipulator.js'; @@ -132,22 +127,16 @@ export default class View extends Component { this.containerRef = React.createRef(); // Create vtk.js view - this.renderWindow = vtkRenderWindow.newInstance(); - this.renderer = vtkRenderer.newInstance(); - this.renderWindow.addRenderer(this.renderer); + this.renderWindow = props.renderWindow; + this.renderer = props.renderer; this.camera = this.renderer.getActiveCamera(); - this.openglRenderWindow = vtkOpenGLRenderWindow.newInstance(); - this.renderWindow.addView(this.openglRenderWindow); + this.openglRenderWindow = props.renderWindowView; if (props.interactive) { - this.interactor = vtkRenderWindowInteractor.newInstance(); - this.interactor.setView(this.openglRenderWindow); - this.interactor.initialize(); - - // Interactor style - this.style = vtkInteractorStyleManipulator.newInstance(); - this.interactor.setInteractorStyle(this.style); + this.interactor = props.interactor; + this.defaultStyle = vtkInteractorStyleManipulator.newInstance(); + this.style = this.defaultStyle; } // Create orientation widget @@ -155,8 +144,8 @@ export default class View extends Component { this.orientationWidget = vtkOrientationMarkerWidget.newInstance({ actor: this.axesActor, interactor: this.interactor, + parentRenderer: this.renderer, }); - this.orientationWidget.setEnabled(true); this.orientationWidget.setViewportCorner( vtkOrientationMarkerWidget.Corners.BOTTOM_LEFT ); @@ -204,6 +193,11 @@ export default class View extends Component { }; this.debouncedCubeBounds = debounce(this.updateCubeBounds, 50); + this.setInteractorStyle = (style) => { + this.style = style; + this.interactor.setInteractorStyle(style); + }; + // Internal functions this.hasFocus = false; this.handleKey = (e) => { @@ -345,7 +339,7 @@ export default class View extends Component { } getScreenEventPositionFor(source) { - const bounds = this.containerRef.current.getBoundingClientRect(); + const bounds = this.openglRenderWindow.getCanvas().getBoundingClientRect(); const [canvasWidth, canvasHeight] = this.openglRenderWindow.getSize(); const scaleX = canvasWidth / bounds.width; const scaleY = canvasHeight / bounds.height; @@ -357,6 +351,10 @@ export default class View extends Component { return position; } + onResize() { + this.props.onResize(); + } + render() { const { id, children, style, className } = this.props; @@ -373,35 +371,22 @@ export default class View extends Component { onMouseMove={this.onMouseMove} >
-
- {children} -
+ {children}
); } - onResize() { - const container = this.containerRef.current; - if (container) { - const devicePixelRatio = window.devicePixelRatio || 1; - const { width, height } = container.getBoundingClientRect(); - const w = Math.floor(width * devicePixelRatio); - const h = Math.floor(height * devicePixelRatio); - this.openglRenderWindow.setSize(Math.max(w, 10), Math.max(h, 10)); - this.renderWindow.render(); - } - } - componentDidMount() { const container = this.containerRef.current; - this.openglRenderWindow.setContainer(container); - if (this.props.interactive) { - this.interactor.bindEvents(container); - } this.onResize(); this.resizeObserver.observe(container); - this.update(this.props); document.addEventListener('keyup', this.handleKey); + + // Assign the mouseDown event, we can't use the React event system + // because the mouseDown event is swallowed by other logic + container.addEventListener('mousedown', this.onMouseDown); + + this.update(this.props); this.resetCamera(); // Give a chance for the first layout to properly reset the camera @@ -423,34 +408,23 @@ export default class View extends Component { this.subscriptions.pop().unsubscribe(); } + const container = this.containerRef.current; + container.removeEventListener('mousedown', this.onMouseDown); + document.removeEventListener('keyup', this.handleKey); // Stop size listening this.resizeObserver.disconnect(); this.resizeObserver = null; - // Detatch from DOM - if (this.interactor) { - this.interactor.unbindEvents(); - } - this.openglRenderWindow.setContainer(null); + this.selector.delete(); + this.orientationWidget.delete(); + this.defaultStyle.delete(); - // Free memory - this.renderWindow.removeRenderer(this.renderer); - this.renderWindow.removeView(this.openglRenderWindow); - - if (this.interactor) { - this.interactor.delete(); - this.interactor = null; - } - - this.renderer.delete(); + this.defaultStyle = null; + this.style = null; this.renderer = null; - - this.renderWindow.delete(); - this.renderWindow = null; - - this.openglRenderWindow.delete(); - this.openglRenderWindow = null; + this.selector = null; + this.orientationWidget = null; } update(props, previous) { @@ -527,11 +501,6 @@ export default class View extends Component { if (previous && triggerResetCamera !== previous.triggerResetCamera) { this.resetCameraTimeout = setTimeout(this.resetCamera, 0); } - - // Assign the mouseDown event, we can't use the React event system - // because the mouseDown event is swallowed by other logic - const canvas = this.openglRenderWindow.getCanvas(); - canvas.addEventListener('mousedown', this.onMouseDown); } resetCamera() { diff --git a/src/core/ViewContainer.js b/src/core/ViewContainer.js new file mode 100644 index 0000000..90bee5b --- /dev/null +++ b/src/core/ViewContainer.js @@ -0,0 +1,170 @@ +import React, { Component } from 'react'; + +import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow.js'; +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow.js'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor.js'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer.js'; + +import View from './View'; +import { MultiViewRootContext } from './MultiViewRoot'; + +class ViewController extends Component { + constructor(props) { + super(props); + + this.renderer = vtkRenderer.newInstance(); + this.viewRef = React.createRef(); + + if (props.root) { + this.renderWindow = props.root.renderWindow; + this.openglRenderWindow = props.root.renderWindowView; + this.interactor = props.root.interactor; + } else { + this.renderWindow = vtkRenderWindow.newInstance(); + this.openglRenderWindow = vtkOpenGLRenderWindow.newInstance(); + } + + this.onEnter = this.onEnter.bind(this); + this.onResize = this.onResize.bind(this); + } + + componentDidMount() { + if (!this.props.root) { + this.renderWindow.addView(this.openglRenderWindow); + + this.interactor = vtkRenderWindowInteractor.newInstance(); + if (this.props.interactive) { + this.interactor.setView(this.openglRenderWindow); + this.interactor.initialize(); + } + } + this.renderWindow.addRenderer(this.renderer); + + const view = this.viewRef.current; + const container = view.containerRef.current; + container.addEventListener('pointerenter', this.onEnter); + + if (!this.props.root) { + this.openglRenderWindow.setContainer(container); + if (this.props.interactive) { + this.interactor.bindEvents(container); + } + this.interactor.setInteractorStyle(view.style); + } + } + + componentWillUnmount() { + const view = this.viewRef.current; + const container = view.containerRef.current; + container.removeEventListener('pointerenter', this.onEnter); + + // MultiViewRoot parent may delete the render window first in WillUnmount. + if (!this.renderWindow.isDeleted()) { + this.renderWindow.removeRenderer(this.renderer); + } + + if (this.props.root) { + this.bindInteractorEvents(null); + } else { + // Detatch from DOM + if (this.interactor.getContainer()) { + this.interactor.unbindEvents(); + } + this.openglRenderWindow.setContainer(null); + + if (!this.renderWindow.isDeleted()) { + this.renderWindow.removeView(this.openglRenderWindow); + this.renderWindow.delete(); + } + + this.interactor.delete(); + this.openglRenderWindow.delete(); + } + + this.renderer.delete(); + + this.interactor = null; + this.renderWindow = null; + this.openglRenderWindow = null; + } + + render() { + const filteredProps = { ...this.props }; + delete filteredProps.root; + + return ( + + ); + } + + bindInteractorEvents(el) { + const oldContainer = this.interactor.getContainer(); + if (oldContainer !== el) { + if (oldContainer) { + this.interactor.unbindEvents(); + } + if (el) { + this.interactor.bindEvents(el); + } + } + } + + onEnter() { + const view = this.viewRef.current; + const container = view?.containerRef.current; + if (this.props.root && container) { + this.bindInteractorEvents(container); + this.interactor.setCurrentRenderer(this.renderer); + this.interactor.setInteractorStyle(view.style); + } + } + + onResize() { + const container = this.viewRef.current?.containerRef.current; + if (container) { + if (this.props.root) { + const containerBox = container.getBoundingClientRect(); + const canvasBox = this.openglRenderWindow + .getCanvas() + .getBoundingClientRect(); + + // relative to canvas + const top = containerBox.top - canvasBox.top; + const left = containerBox.left - canvasBox.left; + + const xmin = left / canvasBox.width; + const xmax = (left + containerBox.width) / canvasBox.width; + const ymin = 1 - (top + containerBox.height) / canvasBox.height; + const ymax = 1 - top / canvasBox.height; + + this.renderer.setViewport(xmin, ymin, xmax, ymax); + } else { + const devicePixelRatio = window.devicePixelRatio || 1; + const { width, height } = container.getBoundingClientRect(); + const w = Math.floor(width * devicePixelRatio); + const h = Math.floor(height * devicePixelRatio); + this.openglRenderWindow.setSize(Math.max(w, 10), Math.max(h, 10)); + this.renderWindow.render(); + } + } + } +} + +ViewController.defaultProps = View.defaultProps; +ViewController.propTypes = View.propTypes; + +export default function ViewContainer(props) { + return ( + + {(root) => } + + ); +} diff --git a/src/core/index.js b/src/core/index.js index e646c07..bc371ed 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -5,7 +5,7 @@ import vtkPointData from './PointData'; import vtkPolyData from './PolyData'; import vtkReader from './Reader'; import vtkShareDataSet from './ShareDataSet'; -import vtkView from './View'; +import vtkView from './ViewContainer'; import vtkGeometryRepresentation from './GeometryRepresentation'; import vtkGeometry2DRepresentation from './Geometry2DRepresentation'; import vtkGlyphRepresentation from './GlyphRepresentation'; @@ -15,6 +15,7 @@ import vtkFieldData from './FieldData'; import vtkAlgorithm from './Algorithm'; import vtkCalculator from './Calculator'; import vtkCellData from './CellData'; +import vtkMultiViewRoot from './MultiViewRoot'; export const VolumeRepresentation = vtkVolumeRepresentation; export const SliceRepresentation = vtkSliceRepresentation; @@ -33,6 +34,7 @@ export const FieldData = vtkFieldData; export const Algorithm = vtkAlgorithm; export const Calculator = vtkCalculator; export const CellData = vtkCellData; +export const MultiViewRoot = vtkMultiViewRoot; export default { VolumeRepresentation: vtkVolumeRepresentation, @@ -52,4 +54,5 @@ export default { Algorithm: vtkAlgorithm, Calculator: vtkCalculator, CellData: vtkCellData, + MultiViewRoot: vtkMultiViewRoot, }; diff --git a/src/index.js b/src/index.js index c077c6c..9b99d12 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ export const FieldData = Core.FieldData; export const Algorithm = Core.Algorithm; export const Calculator = Core.Calculator; export const CellData = Core.CellData; +export const MultiViewRoot = Core.MultiViewRoot; // Representations export const PointCloudRepresentation = diff --git a/usage/src/App.jsx b/usage/src/App.jsx index 8401802..2d435b7 100644 --- a/usage/src/App.jsx +++ b/usage/src/App.jsx @@ -17,6 +17,7 @@ const SyntheticVolumeRendering = lazy(() => ); const VolumeRendering = lazy(() => import('./Volume/VolumeRendering')); const DynamicUpdate = lazy(() => import('./Volume/DynamicUpdate')); +const MultiView = lazy(() => import('./MultiView')); const demos = [ 'Geometry/Picking', @@ -32,6 +33,7 @@ const demos = [ 'Volume/SyntheticVolumeRendering', 'Volume/VolumeRendering', 'Volume/DynamicUpdate', + 'MultiView', ]; function App() { @@ -93,6 +95,7 @@ function App() { )} {example === 'Volume/VolumeRendering' && } {example === 'Volume/DynamicUpdate' && } + {example === 'MultiView' && }
diff --git a/usage/src/Geometry/CubeAxes.jsx b/usage/src/Geometry/CubeAxes.jsx index 530d3b8..cc4f90b 100644 --- a/usage/src/Geometry/CubeAxes.jsx +++ b/usage/src/Geometry/CubeAxes.jsx @@ -30,7 +30,7 @@ function Example(props) { zIndex: 1, }; return ( -
+
diff --git a/usage/src/Geometry/CutterExample.jsx b/usage/src/Geometry/CutterExample.jsx index fa654bc..cf0faa2 100644 --- a/usage/src/Geometry/CutterExample.jsx +++ b/usage/src/Geometry/CutterExample.jsx @@ -11,7 +11,7 @@ function Example(props) { }); return ( -
+
+
+
+
+    
+
+
+
+
diff --git a/usage/src/Geometry/TubeExample.jsx b/usage/src/Geometry/TubeExample.jsx index 5a0e06e..e4d3f9c 100644 --- a/usage/src/Geometry/TubeExample.jsx +++ b/usage/src/Geometry/TubeExample.jsx @@ -8,7 +8,7 @@ import { function Example(props) { return ( -
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + ); +} + +export default Example; diff --git a/usage/src/Volume/DynamicRepUpdate.jsx b/usage/src/Volume/DynamicRepUpdate.jsx index 85fb07d..429fff1 100644 --- a/usage/src/Volume/DynamicRepUpdate.jsx +++ b/usage/src/Volume/DynamicRepUpdate.jsx @@ -76,7 +76,7 @@ function Example(props) { const colorLevel = fieldIdx ? 5 : 0.5; return ( -
+
diff --git a/usage/src/Volume/DynamicUpdate.jsx b/usage/src/Volume/DynamicUpdate.jsx index b453a8b..7d109b6 100644 --- a/usage/src/Volume/DynamicUpdate.jsx +++ b/usage/src/Volume/DynamicUpdate.jsx @@ -76,7 +76,7 @@ function Example(props) { const colorLevel = fieldIdx ? 5 : 0.5; return ( -
+
diff --git a/usage/src/Volume/SliceRendering.jsx b/usage/src/Volume/SliceRendering.jsx index 7bf4b8e..b0c3d75 100644 --- a/usage/src/Volume/SliceRendering.jsx +++ b/usage/src/Volume/SliceRendering.jsx @@ -116,8 +116,8 @@ function Example(props) { const [useLookupTableScalarRange, setUseLookupTableScalarRange] = useState(false); return ( -
-
+
+
-
+
+
+