diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 21ae84962f..f337dc33a3 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -1571,7 +1571,7 @@ export class CrosshairsTool extends AnnotationTool { // (undocumented) _filterViewportWithSameOrientation: (enabledElement: any, referenceAnnotation: any, annotations: any) => any; // (undocumented) - _getAnnotations: (enabledElement: Types_2.IEnabledElement) => Annotations; + _getAnnotations: (enabledElement: Types_2.IEnabledElement) => Annotation[]; // (undocumented) _getAnnotationsForViewportsWithDifferentCameras: (enabledElement: any, annotations: any) => any; // (undocumented) @@ -3233,7 +3233,7 @@ function isViewportPreScaled(viewport: Types_2.IStackViewport | Types_2.IVolumeV // @public (undocumented) interface ISynchronizerEventHandler { // (undocumented) - (synchronizer: Synchronizer, sourceViewport: Types_2.IViewportId, targetViewport: Types_2.IViewportId, sourceEvent: any, options?: any): void; + (synchronizer: Synchronizer, sourceViewport: Types_2.IViewportId, targetViewport: Types_2.IViewportId, sourceEvent: any, options?: any): Promise | void; } // @public (undocumented) diff --git a/packages/core/src/Settings.ts b/packages/core/src/Settings.ts index f54f9ab085..f5b2b261ee 100644 --- a/packages/core/src/Settings.ts +++ b/packages/core/src/Settings.ts @@ -13,7 +13,9 @@ const DICTIONARY = Symbol('Dictionary'); export default class Settings { constructor(base?: Settings) { const dictionary = Object.create( - base instanceof Settings && DICTIONARY in base ? base[DICTIONARY] : null + (base instanceof Settings && DICTIONARY in base + ? base[DICTIONARY] + : null) as object ); Object.seal( Object.defineProperty(this, DICTIONARY, { diff --git a/packages/tools/src/store/SynchronizerManager/Synchronizer.ts b/packages/tools/src/store/SynchronizerManager/Synchronizer.ts index b6d750e7d3..609c94b56a 100644 --- a/packages/tools/src/store/SynchronizerManager/Synchronizer.ts +++ b/packages/tools/src/store/SynchronizerManager/Synchronizer.ts @@ -215,15 +215,19 @@ class Synchronizer { if (targetIsSource) { continue; } - promises.push( - this._eventHandler( - this, - sourceViewport, - targetViewport, - sourceEvent, - this._options - ) + const result = this._eventHandler( + this, + sourceViewport, + targetViewport, + sourceEvent, + this._options ); + + // if the result is a promise, then add it to the list of promises + // to wait for before setting _ignoreFiredEvents to false + if (result instanceof Promise) { + promises.push(result); + } } } catch (ex) { console.warn(`Synchronizer, for: ${this._eventName}`, ex); diff --git a/packages/tools/src/tools/CrosshairsTool.ts b/packages/tools/src/tools/CrosshairsTool.ts index ea6c82fdca..514ef143bc 100644 --- a/packages/tools/src/tools/CrosshairsTool.ts +++ b/packages/tools/src/tools/CrosshairsTool.ts @@ -1432,7 +1432,19 @@ class CrosshairsTool extends AnnotationTool { _getAnnotations = (enabledElement: Types.IEnabledElement) => { const { viewport } = enabledElement; - return getAnnotations(this.getToolName(), viewport.element); + const annotations = + getAnnotations(this.getToolName(), viewport.element) || []; + const viewportIds = this._getViewportsInfo().map( + ({ viewportId }) => viewportId + ); + + // filter the annotations to only keep that are for this toolGroup + const toolGroupAnnotations = annotations.filter((annotation) => { + const { data } = annotation; + return viewportIds.includes(data.viewportId); + }); + + return toolGroupAnnotations; }; _onNewVolume = (e: any) => { diff --git a/packages/tools/src/types/ISynchronizerEventHandler.ts b/packages/tools/src/types/ISynchronizerEventHandler.ts index 46e57db38d..077a563493 100644 --- a/packages/tools/src/types/ISynchronizerEventHandler.ts +++ b/packages/tools/src/types/ISynchronizerEventHandler.ts @@ -8,5 +8,5 @@ export default interface ISynchronizerEventHandler { targetViewport: Types.IViewportId, sourceEvent: any, options?: any - ): void; + ): Promise | void; } diff --git a/packages/tools/test/CrosshairsTool_test.js b/packages/tools/test/CrosshairsTool_test.js index a65a429489..62ac3c74f4 100644 --- a/packages/tools/test/CrosshairsTool_test.js +++ b/packages/tools/test/CrosshairsTool_test.js @@ -21,13 +21,15 @@ const { Events, ViewportType } = Enums; const { unregisterAllImageLoaders } = imageLoader; const { registerVolumeLoader, createAndCacheVolume } = volumeLoader; - const { CrosshairsTool, ToolGroupManager, Enums: csToolsEnums, annotation, + synchronizers, } = csTools3d; +const { createCameraPositionSynchronizer, createVOISynchronizer } = + synchronizers; const { Events: csToolsEvents } = csToolsEnums; @@ -38,7 +40,11 @@ const renderingEngineId = utilities.uuidv4(); const viewportId1 = 'VIEWPORT1'; const viewportId2 = 'VIEWPORT2'; -const viewportUID3 = 'VIEWPORT3'; +const viewportId3 = 'VIEWPORT3'; +// +const viewportId4 = 'VIEWPORT4'; +const viewportId5 = 'VIEWPORT5'; +const viewportId6 = 'VIEWPORT6'; const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0`; @@ -61,35 +67,6 @@ function createViewports(renderingEngine, viewportType, width, height) { element3.style.height = `${height}px`; document.body.appendChild(element3); - renderingEngine.setViewports([ - { - viewportId: viewportId1, - type: viewportType, - element: element1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: Enums.OrientationAxis.AXIAL, - }, - }, - { - viewportId: viewportId2, - type: viewportType, - element: element2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: Enums.OrientationAxis.SAGITTAL, - }, - }, - { - viewportId: viewportUID3, - type: viewportType, - element: element3, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: Enums.OrientationAxis.CORONAL, - }, - }, - ]); return [element1, element2, element3]; } @@ -138,6 +115,37 @@ describe('Cornerstone Tools: ', () => { 512, 128 ); + + this.renderingEngine.setViewports([ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]); + this.DOMElements.push(element1); this.DOMElements.push(element2); this.DOMElements.push(element3); @@ -220,14 +228,14 @@ describe('Cornerstone Tools: ', () => { this.testToolGroup.addViewport(viewportId1, this.renderingEngine.id); this.testToolGroup.addViewport(viewportId2, this.renderingEngine.id); - this.testToolGroup.addViewport(viewportUID3, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId3, this.renderingEngine.id); try { createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { setVolumesForViewports( this.renderingEngine, [{ volumeId: volumeId }], - [viewportId1, viewportId2, viewportUID3] + [viewportId1, viewportId2, viewportId3] ); this.renderingEngine.render(); }); @@ -243,6 +251,37 @@ describe('Cornerstone Tools: ', () => { 512, 128 ); + + this.renderingEngine.setViewports([ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]); + this.DOMElements.push(element1); this.DOMElements.push(element2); this.DOMElements.push(element3); @@ -367,14 +406,14 @@ describe('Cornerstone Tools: ', () => { this.testToolGroup.addViewport(viewportId1, this.renderingEngine.id); this.testToolGroup.addViewport(viewportId2, this.renderingEngine.id); - this.testToolGroup.addViewport(viewportUID3, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId3, this.renderingEngine.id); try { createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { setVolumesForViewports( this.renderingEngine, [{ volumeId: volumeId }], - [viewportId1, viewportId2, viewportUID3] + [viewportId1, viewportId2, viewportId3] ); this.renderingEngine.render(); }); @@ -390,6 +429,37 @@ describe('Cornerstone Tools: ', () => { 512, 128 ); + + this.renderingEngine.setViewports([ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]); + this.DOMElements.push(element1); this.DOMElements.push(element2); this.DOMElements.push(element3); @@ -510,14 +580,448 @@ describe('Cornerstone Tools: ', () => { this.testToolGroup.addViewport(viewportId1, this.renderingEngine.id); this.testToolGroup.addViewport(viewportId2, this.renderingEngine.id); - this.testToolGroup.addViewport(viewportUID3, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId3, this.renderingEngine.id); + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesForViewports( + this.renderingEngine, + [{ volumeId: volumeId }], + [viewportId1, viewportId2, viewportId3] + ); + this.renderingEngine.render(); + }); + } catch (e) { + done.fail(e); + } + }); +}); + +describe('Crosshairs with synchronizers: ', () => { + beforeAll(() => { + // initialize the library + cornerstone3D.setUseCPURendering(false); + }); + + beforeEach(function () { + csTools3d.init(); + csTools3d.addTool(CrosshairsTool); + cache.purgeCache(); + this.DOMElements = []; + + this.testToolGroup = ToolGroupManager.createToolGroup('volume'); + this.testToolGroup.addTool(CrosshairsTool.toolName, { + configuration: {}, + }); + this.testToolGroup1 = ToolGroupManager.createToolGroup('volume1'); + this.testToolGroup1.addTool(CrosshairsTool.toolName, { + configuration: {}, + }); + + this.renderingEngine = new RenderingEngine(renderingEngineId); + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + csTools3d.destroy(); + + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + ToolGroupManager.destroyToolGroup('volume'); + ToolGroupManager.destroyToolGroup('volume1'); + + this.DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should be able to have two separate crosshairs for different toolGroups', function (done) { + const [element1, element2, element3] = createViewports( + this.renderingEngine, + ViewportType.ORTHOGRAPHIC, + 512, + 128 + ); + const [element4, element5, element6] = createViewports( + this.renderingEngine, + ViewportType.ORTHOGRAPHIC, + 512, + 128 + ); + + this.DOMElements.push(element1); + this.DOMElements.push(element2); + this.DOMElements.push(element3); + this.DOMElements.push(element4); + this.DOMElements.push(element5); + this.DOMElements.push(element6); + + this.renderingEngine.setViewports([ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + { + viewportId: viewportId4, + type: ViewportType.ORTHOGRAPHIC, + element: element4, + defaultOptions: { + background: [0, 0, 1], + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId5, + type: ViewportType.ORTHOGRAPHIC, + element: element5, + defaultOptions: { + background: [0, 0, 1], + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId6, + type: ViewportType.ORTHOGRAPHIC, + element: element6, + defaultOptions: { + background: [0, 0, 1], + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]); + + let canvasesRendered = 0; + const renderEventHandler = () => { + canvasesRendered += 1; + + if (canvasesRendered !== 6) { + return; + } + + this.testToolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: 1 }], + }); + this.testToolGroup1.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: 1 }], + }); + + setTimeout(() => { + // get the toolCenter for the third viewport + const vp4 = this.renderingEngine.getViewport(viewportId3); + const crosshairAnnotations = annotation.state.getAnnotations( + CrosshairsTool.toolName, + element4 + ); + + // find the annotation for vp3 + const annotationForVp4 = crosshairAnnotations.find((annotation) => { + return annotation.data.viewportId === vp4.id; + }); + + const toolCenter = annotationForVp4.data.handles.toolCenter; + + // click on the first viewport + const index1 = [32, 32, 0]; + + const vp1 = this.renderingEngine.getViewport(viewportId1); + const { imageData } = vp1.getImageData(); + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + } = createNormalizedMouseEvent(imageData, index1, element1, vp1); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element1, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + element1.dispatchEvent(evt); + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + setTimeout(() => { + // get the vp4 toolCenter and it should have been not changed + const vp4ToolCenter = annotation.state + .getAnnotations(CrosshairsTool.toolName, element4) + .find((annotation) => { + return annotation.data.viewportId === vp4.id; + }); + + vp4ToolCenter.data.handles.toolCenter.forEach((p, i) => { + expect(p).toBeCloseTo(toolCenter[i], 3); + }); + + done(); + }, 1000); + + // done(); + }, 500); + }; + + element1.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element2.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element3.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + + element4.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element5.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element6.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + + this.testToolGroup.addViewport(viewportId1, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId2, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId3, this.renderingEngine.id); + this.testToolGroup1.addViewport(viewportId4, this.renderingEngine.id); + this.testToolGroup1.addViewport(viewportId5, this.renderingEngine.id); + this.testToolGroup1.addViewport(viewportId6, this.renderingEngine.id); + + this.renderingEngine.render(); try { createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { setVolumesForViewports( this.renderingEngine, [{ volumeId: volumeId }], - [viewportId1, viewportId2, viewportUID3] + [viewportId1, viewportId2, viewportId3] + ); + setVolumesForViewports( + this.renderingEngine, + [{ volumeId: volumeId }], + [viewportId4, viewportId5, viewportId6] + ); + this.renderingEngine.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully work with camera synchronizers on', function (done) { + const [element1, element2, element3] = createViewports( + this.renderingEngine, + ViewportType.ORTHOGRAPHIC, + 512, + 128 + ); + const [element4, element5, element6] = createViewports( + this.renderingEngine, + ViewportType.ORTHOGRAPHIC, + 512, + 128 + ); + + this.DOMElements.push(element1); + this.DOMElements.push(element2); + this.DOMElements.push(element3); + this.DOMElements.push(element4); + this.DOMElements.push(element5); + this.DOMElements.push(element6); + + this.renderingEngine.setViewports([ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + { + viewportId: viewportId4, + type: ViewportType.ORTHOGRAPHIC, + element: element4, + defaultOptions: { + background: [0, 0, 1], + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId5, + type: ViewportType.ORTHOGRAPHIC, + element: element5, + defaultOptions: { + background: [0, 0, 1], + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId6, + type: ViewportType.ORTHOGRAPHIC, + element: element6, + defaultOptions: { + background: [0, 0, 1], + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]); + + let canvasesRendered = 0; + const renderEventHandler = () => { + canvasesRendered += 1; + + if (canvasesRendered !== 6) { + return; + } + + this.testToolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: 1 }], + }); + this.testToolGroup1.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: 1 }], + }); + + setTimeout(() => { + // get the toolCenter for the third viewport + const vp5 = this.renderingEngine.getViewport(viewportId5); + const crosshairAnnotations = annotation.state.getAnnotations( + CrosshairsTool.toolName, + element5 + ); + + // find the annotation for vp3 + const annotationForVp5 = crosshairAnnotations.find((annotation) => { + return annotation.data.viewportId === vp5.id; + }); + + const oldToolCenter = JSON.parse( + JSON.stringify(annotationForVp5.data.handles.toolCenter) + ); + + // click on the first viewport + const index1 = [32, 32, 0]; + + const vp3 = this.renderingEngine.getViewport(viewportId3); + const { imageData } = vp3.getImageData(); + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + } = createNormalizedMouseEvent(imageData, index1, element3, vp3); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element3, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + element3.dispatchEvent(evt); + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + setTimeout(() => { + // get the vp5 toolCenter should have changed + const vp5ToolCenter = annotation.state + .getAnnotations(CrosshairsTool.toolName, element5) + .find((annotation) => { + return annotation.data.viewportId === vp5.id; + }); + + expect(vp5ToolCenter.data.handles.toolCenter[2]).not.toBeCloseTo( + oldToolCenter[2], + 3 + ); + + done(); + }, 500); + + // done(); + }, 500); + }; + + element1.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element2.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element3.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + + element4.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element5.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + element6.addEventListener(Events.IMAGE_RENDERED, renderEventHandler); + + this.testToolGroup.addViewport(viewportId1, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId2, this.renderingEngine.id); + this.testToolGroup.addViewport(viewportId3, this.renderingEngine.id); + + this.testToolGroup1.addViewport(viewportId4, this.renderingEngine.id); + this.testToolGroup1.addViewport(viewportId5, this.renderingEngine.id); + this.testToolGroup1.addViewport(viewportId6, this.renderingEngine.id); + + const axialSync = createCameraPositionSynchronizer('axialSync'); + + axialSync.add({ + renderingEngineId: this.renderingEngine.id, + viewportId: this.renderingEngine.getViewport(viewportId1).id, + }); + axialSync.add({ + renderingEngineId: this.renderingEngine.id, + viewportId: this.renderingEngine.getViewport(viewportId4).id, + }); + + this.renderingEngine.render(); + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesForViewports( + this.renderingEngine, + [{ volumeId: volumeId }], + [viewportId1, viewportId2, viewportId3] + ); + setVolumesForViewports( + this.renderingEngine, + [{ volumeId: volumeId }], + [viewportId4, viewportId5, viewportId6] ); this.renderingEngine.render(); });