diff --git a/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts b/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts index 7a8cbcc150..e1d961e7bd 100644 --- a/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts +++ b/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts @@ -159,9 +159,6 @@ class RenderingEngine implements IRenderingEngine { // 3 Add the requested viewport to rendering Engine this.addCustomViewport(viewportInputEntry) } - - // 5. Add the new viewport to the queue to be rendered - this._setViewportsToBeRenderedNextFrame([viewportInputEntry.viewportUID]) } /** @@ -733,7 +730,9 @@ class RenderingEngine implements IRenderingEngine { renderingEngineUID: this.uid, } - triggerEvent(eventTarget, EVENTS.ELEMENT_ENABLED, eventData) + if (!viewport.suppressEvents) { + triggerEvent(eventTarget, EVENTS.ELEMENT_ENABLED, eventData) + } } /** @@ -1164,6 +1163,7 @@ class RenderingEngine implements IRenderingEngine { viewportUID: string sceneUID: string renderingEngineUID: string + suppressEvents: boolean } { const { element, @@ -1175,6 +1175,7 @@ class RenderingEngine implements IRenderingEngine { uid, sceneUID, renderingEngineUID, + suppressEvents, } = viewport const { width: dWidth, height: dHeight } = canvas @@ -1197,6 +1198,7 @@ class RenderingEngine implements IRenderingEngine { element, canvas, viewportUID: uid, + suppressEvents, sceneUID, renderingEngineUID, } @@ -1212,7 +1214,7 @@ class RenderingEngine implements IRenderingEngine { private _resetViewport(viewport) { const renderingEngineUID = this.uid - const { element, canvas, uid: viewportUID } = viewport + const { element, canvas, uid: viewportUID, suppressEvents } = viewport const eventData = { element, @@ -1223,7 +1225,9 @@ class RenderingEngine implements IRenderingEngine { // Trigger first before removing the data attributes, as we need the enabled // element to remove tools associated with the viewport - triggerEvent(eventTarget, EVENTS.ELEMENT_DISABLED, eventData) + if (!suppressEvents) { + triggerEvent(eventTarget, EVENTS.ELEMENT_DISABLED, eventData) + } element.removeAttribute('data-viewport-uid') element.removeAttribute('data-scene-uid') diff --git a/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts b/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts index 1bf2f08e59..5272f87ec8 100644 --- a/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts @@ -1058,7 +1058,13 @@ class StackViewport extends Viewport { delete this._cpuFallbackEnabledElement.viewport.colormap } + // We draw over the previous stack with the background color while we + // wait for the next stack to load + ctx.fillStyle = fillStyle + ctx.fillRect(0, 0, canvas.width, canvas.height) + const imageId = await this._setImageIdIndex(currentImageIdIndex) + return imageId } @@ -1250,7 +1256,10 @@ class StackViewport extends Viewport { imageId, } - triggerEvent(eventTarget, ERROR_CODES.IMAGE_LOAD_ERROR, eventData) + if (!this.suppressEvents) { + triggerEvent(eventTarget, ERROR_CODES.IMAGE_LOAD_ERROR, eventData) + } + reject(error) } @@ -1283,7 +1292,7 @@ class StackViewport extends Viewport { const type = 'Float32Array' const priority = -5 - const requestType = 'interaction' + const requestType = REQUEST_TYPE.Interaction const additionalDetails = { imageId } const options = { targetBuffer: { @@ -1296,7 +1305,7 @@ class StackViewport extends Viewport { }, } - requestPoolManager.addRequest( + imageLoadPoolManager.addRequest( sendRequest.bind(this, imageId, imageIdIndex, options), requestType, additionalDetails, @@ -1531,13 +1540,11 @@ class StackViewport extends Viewport { // Update the state of the viewport to the new imageIdIndex; this.currentImageIdIndex = imageIdIndex - // Get the imageId from the stack - const imageId = this.imageIds[imageIdIndex] - // Todo: trigger an event to allow applications to hook into START of loading state // Currently we use loadHandlerManagers for this - await this._loadImage(imageId, imageIdIndex) + const imageId = this._loadImage(this.imageIds[imageIdIndex], imageIdIndex) + return imageId } @@ -1574,14 +1581,16 @@ class StackViewport extends Viewport { * @param imageIdIndex number represents imageId index in the list of * provided imageIds in setStack */ - public setImageIdIndex(imageIdIndex: number): void { + public async setImageIdIndex(imageIdIndex: number): Promise { // If we are already on this imageId index, stop here if (this.currentImageIdIndex === imageIdIndex) { return } // Otherwise, get the imageId and attempt to display it - this._setImageIdIndex(imageIdIndex) + const imageId = this._setImageIdIndex(imageIdIndex) + + return imageId } /** @@ -1657,8 +1666,10 @@ class StackViewport extends Viewport { renderingEngineUID: this.renderingEngineUID, } - // For crosshairs to adapt to new viewport size - triggerEvent(this.element, EVENTS.CAMERA_MODIFIED, eventDetail) + if (!this.suppressEvents) { + // For crosshairs to adapt to new viewport size + triggerEvent(this.element, EVENTS.CAMERA_MODIFIED, eventDetail) + } } private triggerCalibrationEvent() { @@ -1678,8 +1689,10 @@ class StackViewport extends Viewport { worldToIndex: imageData.getWorldToIndex(), } - // Let the tools know the image spacing has been calibrated - triggerEvent(this.element, EVENTS.IMAGE_SPACING_CALIBRATED, eventDetail) + if (!this.suppressEvents) { + // Let the tools know the image spacing has been calibrated + triggerEvent(this.element, EVENTS.IMAGE_SPACING_CALIBRATED, eventDetail) + } this._publishCalibratedEvent = false } diff --git a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts index b7c9b8d721..69f22d11ee 100644 --- a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts @@ -36,6 +36,7 @@ class Viewport { readonly defaultOptions: any options: ViewportInputOptions private _suppressCameraModifiedEvents = false + readonly suppressEvents: boolean constructor(props: ViewportInput) { this.uid = props.uid @@ -61,8 +62,12 @@ class Viewport { } this.defaultOptions = _cloneDeep(props.defaultOptions) + this.suppressEvents = props.defaultOptions.suppressEvents + ? props.defaultOptions.suppressEvents + : false this.options = _cloneDeep(props.defaultOptions) } + getFrameOfReferenceUID: () => string canvasToWorld: (canvasPos: Point2) => Point3 worldToCanvas: (worldPos: Point3) => Point2 @@ -521,7 +526,7 @@ class Viewport { // and do the right thing. renderer.invokeEvent(RESET_CAMERA_EVENT) - if (!this._suppressCameraModifiedEvents) { + if (!this._suppressCameraModifiedEvents && !this.suppressEvents) { const eventDetail = { previousCamera: previousCamera, camera: this.getCamera(), @@ -680,7 +685,7 @@ class Viewport { vtkCamera.setSlabThickness(slabThickness) } - if (!this._suppressCameraModifiedEvents) { + if (!this._suppressCameraModifiedEvents && !this.suppressEvents) { const eventDetail = { previousCamera, camera: updatedCamera, diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/index.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/index.ts index c0a17fd672..1d8303632e 100644 --- a/packages/cornerstone-render/src/RenderingEngine/helpers/index.ts +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/index.ts @@ -1,5 +1,11 @@ import createVolumeActor from './createVolumeActor' import createVolumeMapper from './createVolumeMapper' import getOrCreateCanvas from './getOrCreateCanvas' +import renderToCanvas from './renderToCanvas' -export { createVolumeActor, createVolumeMapper, getOrCreateCanvas } +export { + createVolumeActor, + createVolumeMapper, + getOrCreateCanvas, + renderToCanvas, +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/renderToCanvas.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/renderToCanvas.ts new file mode 100644 index 0000000000..89c200453c --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/renderToCanvas.ts @@ -0,0 +1,88 @@ +import { getRenderingEngine } from '../getRenderingEngine' +import getOrCreateCanvas from './getOrCreateCanvas' +import VIEWPORT_TYPE from '../../constants/viewportType' +import ORIENTATION from '../../constants/orientation' +import StackViewport from '../StackViewport' +import Events from '../../enums/events' + +/** + * Renders an imageId to a Canvas Element. This method will handle creation + * of a tempporary enabled element, setting the imageId, and rendering the image via + * a StackViewport, copying the canvas drawing to the given canvas Element, and + * disabling the created temporary element. SuppressEvents argument is used to + * prevent events from firing during the render process (e.g. during a series + * of renders to a thumbnail image). + * @param {string}imageId - The imageId to render + * @param {HTMLCanvasElement} canvas - Canvas element to render to + * @param {string} renderingEngineUID - The rendering engine UID to use + * @param {boolean} suppressEvents - boolean to suppress events during render + * @returns {Promise} - A promise that resolves when the image has been rendered with the imageId + */ +export default function renderToCanvas( + imageId: string, + canvas: HTMLCanvasElement, + renderingEngineUID: string, + suppressEvents = true +): Promise { + return new Promise((resolve, reject) => { + const renderingEngine = getRenderingEngine(renderingEngineUID) + + if (!canvas || !(canvas instanceof HTMLCanvasElement)) { + throw new Error('canvas element is required') + } + + if (!renderingEngine) { + throw new Error( + `No rendering engine with UID of ${renderingEngineUID} found` + ) + } + + if (renderingEngine.hasBeenDestroyed) { + throw new Error( + `Rendering engine with UID of ${renderingEngineUID} has been destroyed` + ) + } + + // Creating a temporary HTML element so that we can + // enable it and later disable it without loosing the canvas context + const element = document.createElement('div') + element.style.width = `${canvas.width}px` + element.style.height = `${canvas.height}px` + + // Todo: we should be able to use the temporary element without appending + // it to the DOM + element.style.visibility = 'hidden' + document.body.appendChild(element) + + // Setting the viewportUID to imageId + const viewportUID = imageId + + const stackViewportInput = { + viewportUID, + type: VIEWPORT_TYPE.STACK, + element, + defaultOptions: { + orientation: ORIENTATION.AXIAL, + suppressEvents, + }, + } + + renderingEngine.enableElement(stackViewportInput) + const viewport = renderingEngine.getViewport(viewportUID) as StackViewport + + element.addEventListener(Events.IMAGE_RENDERED, () => { + // get the canvas element that is the child of the div + const temporaryCanvas = getOrCreateCanvas(element) + + // Copy the temporary canvas to the given canvas + const context = canvas.getContext('2d') + + context.drawImage(temporaryCanvas, 0, 0) + renderingEngine.disableElement(viewportUID) + document.body.removeChild(element) + resolve(imageId) + }) + + viewport.setStack([imageId]) + }) +} diff --git a/packages/cornerstone-render/src/RenderingEngine/index.ts b/packages/cornerstone-render/src/RenderingEngine/index.ts index a3d8595a0a..9606ce758c 100644 --- a/packages/cornerstone-render/src/RenderingEngine/index.ts +++ b/packages/cornerstone-render/src/RenderingEngine/index.ts @@ -4,6 +4,7 @@ import { createVolumeActor, createVolumeMapper, getOrCreateCanvas, + renderToCanvas, } from './helpers' export { @@ -12,6 +13,7 @@ export { createVolumeActor, createVolumeMapper, getOrCreateCanvas, + renderToCanvas, } export default RenderingEngine diff --git a/packages/cornerstone-render/src/index.ts b/packages/cornerstone-render/src/index.ts index f7ed3f330f..f4c83b4452 100644 --- a/packages/cornerstone-render/src/index.ts +++ b/packages/cornerstone-render/src/index.ts @@ -12,6 +12,7 @@ import { createVolumeMapper, getOrCreateCanvas, } from './RenderingEngine' +import { renderToCanvas } from './RenderingEngine' import RenderingEngine from './RenderingEngine' import VolumeViewport from './RenderingEngine/VolumeViewport' import StackViewport from './RenderingEngine/StackViewport' @@ -106,6 +107,7 @@ export { cache, Cache, getEnabledElement, + renderToCanvas, // eventTarget, triggerEvent, diff --git a/packages/cornerstone-render/src/types/IViewport.ts b/packages/cornerstone-render/src/types/IViewport.ts index c08c443892..ca7bbc0f72 100644 --- a/packages/cornerstone-render/src/types/IViewport.ts +++ b/packages/cornerstone-render/src/types/IViewport.ts @@ -44,7 +44,7 @@ interface IViewport { * This type defines the shape of input, so we can throw when it is incorrect. */ type PublicViewportInput = { - element: HTMLDivElement + element: HTMLElement sceneUID?: string viewportUID: string type: string @@ -71,7 +71,7 @@ type ViewportInput = { sy: number sWidth: number sHeight: number - defaultOptions: any + defaultOptions: ViewportInputOptions } export type { diff --git a/packages/cornerstone-render/src/types/ViewportInputOptions.ts b/packages/cornerstone-render/src/types/ViewportInputOptions.ts index d23b889aa6..50a4dec40e 100644 --- a/packages/cornerstone-render/src/types/ViewportInputOptions.ts +++ b/packages/cornerstone-render/src/types/ViewportInputOptions.ts @@ -7,6 +7,7 @@ import Orientation from './Orientation' type ViewportInputOptions = { background?: Array orientation?: Orientation + suppressEvents: boolean } export default ViewportInputOptions diff --git a/packages/demo/src/App.tsx b/packages/demo/src/App.tsx index 6533cc0f0a..91971b968c 100644 --- a/packages/demo/src/App.tsx +++ b/packages/demo/src/App.tsx @@ -22,6 +22,8 @@ import TestUtilsVolume from './ExampleTestUtilsVolume' import CalibrationExample from './ExampleCalibration' import { resetCPURenderingOnlyForDebugOrTests } from '@ohif/cornerstone-render' import SegmentationRender from './ExampleSegmentationRender' +import RenderToCanvasExample from './ExampleRenderToCanvas' + function LinkOut({ href, text }) { return ( @@ -144,6 +146,11 @@ function Index() { url: '/modifierKeys', text: 'Example of using modifier keys', }, + { + title: 'Render To Canvas', + url: '/renderToCanvas', + text: 'Example of rendering an imageId to canvas', + }, { title: 'Test Utils', url: '/testUtils', @@ -311,6 +318,10 @@ function AppRouter() { Example({ children: , }) + const RenderToCanvas = () => + Example({ + children: , + }) const Test = () => Example({ @@ -335,6 +346,7 @@ function AppRouter() { + diff --git a/packages/demo/src/ExampleRenderToCanvas.tsx b/packages/demo/src/ExampleRenderToCanvas.tsx new file mode 100644 index 0000000000..11aa0a00df --- /dev/null +++ b/packages/demo/src/ExampleRenderToCanvas.tsx @@ -0,0 +1,143 @@ +import React, { Component } from 'react' +import { + cache, + RenderingEngine, + renderToCanvas, +} from '@ohif/cornerstone-render' +import * as csTools3d from '@ohif/cornerstone-tools' +import getImageIds from './helpers/getImageIds' +import { renderingEngineUID } from './constants' + +const STACK = 'stack' + +const imageId = + 'wadors:https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1/series/1.3.6.1.4.1.25403.345050719074.3824.20170125095258.2/instances/1.3.6.1.4.1.25403.345050719074.3824.20170125095258.3/frames/1' + +class RenderToCanvasExample extends Component { + state = { + metadataLoaded: false, + petColorMapIndex: 0, + layoutIndex: 0, + destroyed: false, + // + viewportGrid: { + numCols: 1, + numRows: 1, + viewports: [{}], + }, + ptCtLeftClickTool: 'WindowLevel', + ctWindowLevelDisplay: { ww: 0, wc: 0 }, + ptThresholdDisplay: 5, + imageId: imageId, + thumbnailLoaded: false, + } + + constructor(props) { + super(props) + + csTools3d.init() + this._canvasNodes = new Map() + this._offScreenRef = React.createRef() + + this._viewportGridRef = React.createRef() + + this.ctStackImageIdsPromise = getImageIds('ct1', STACK) + this.dxStackImageIdsPromise = getImageIds('dx', STACK) + + Promise.all([this.ctStackImageIdsPromise, this.dxStackImageIdsPromise]) + } + + /** + * LIFECYCLE + */ + async componentDidMount() { + const renderingEngine = new RenderingEngine(renderingEngineUID) + + this.renderingEngine = renderingEngine + window.renderingEngine = renderingEngine + } + + componentDidUpdate(prevProps, prevState) {} + + componentWillUnmount() { + cache.purgeCache() + csTools3d.destroy() + + this.renderingEngine.destroy() + } + + renderToCanvas = (imageId) => { + renderToCanvas(imageId, this._canvasNodes.get(0), renderingEngineUID).then( + () => { + this.setState({ + thumbnailLoaded: true, + }) + } + ) + } + + + render() { + return ( +
+
+
+

Render To Canvas Example

+

+ This example demonstrates how to render an image to a canvas + without dealing with enabling an HTML element. This is useful for + rendering to a thumbnail for instance, which you might not be + interested in using tools or other functionalities. +

+
+
+
+ {/* an input element to put the image Id inside */} +
+
+ + +
+ { + this.setState({ imageId: evt.target.value }) + }} + /> +
+
+
+ {this.state.viewportGrid.viewports.map((vp, i) => ( +
+ this._canvasNodes.set(i, c)} + onContextMenu={(e) => e.preventDefault()} + /> +
+ ))} +
+
+ ) + } +} + +export default RenderToCanvasExample diff --git a/yarn.lock b/yarn.lock index 1d7ac5ff8d..be789190b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3937,7 +3937,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.5: +array.prototype.flat@^1.2.4: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==