diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index d52d7c8bc8..68adc37626 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -1246,10 +1246,14 @@ interface IViewport { // (undocumented) getFrameOfReferenceUID: () => string; // (undocumented) + getPan(): Point2; + // (undocumented) getRenderer(): void; // (undocumented) getRenderingEngine(): any; // (undocumented) + getZoom(): number; + // (undocumented) id: string; // (undocumented) options: ViewportInputOptions; @@ -1264,10 +1268,14 @@ interface IViewport { // (undocumented) setActors(actors: Array): void; // (undocumented) - setCamera(cameraInterface: ICamera): void; + setCamera(cameraInterface: ICamera, storeAsInitialCamera?: boolean): void; // (undocumented) setOptions(options: ViewportInputOptions, immediate: boolean): void; // (undocumented) + setPan(pan: Point2, storeAsInitialCamera?: boolean): any; + // (undocumented) + setZoom(zoom: number, storeAsInitialCamera?: boolean): any; + // (undocumented) sHeight: number; // (undocumented) suppressEvents: boolean; @@ -1951,6 +1959,8 @@ export class Viewport implements IViewport { // (undocumented) getFrameOfReferenceUID: () => string; // (undocumented) + getPan(): Point2; + // (undocumented) getProperties: () => void; // (undocumented) getRenderer(): any; @@ -1959,10 +1969,14 @@ export class Viewport implements IViewport { // (undocumented) protected getVtkActiveCamera(): vtkCamera | vtkSlabCamera; // (undocumented) + getZoom(): number; + // (undocumented) protected hasPixelSpacing: boolean; // (undocumented) readonly id: string; // (undocumented) + protected initialCamera: ICamera; + // (undocumented) _isInBounds(point: Point3, bounds: number[]): boolean; // (undocumented) options: ViewportInputOptions; @@ -1979,7 +1993,7 @@ export class Viewport implements IViewport { // (undocumented) reset(immediate?: boolean): void; // (undocumented) - protected resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + protected resetCamera(resetPan?: boolean, resetZoom?: boolean, storeAsInitialCamera?: boolean): boolean; // (undocumented) protected resetCameraNoEvent(): void; // (undocumented) @@ -1989,14 +2003,20 @@ export class Viewport implements IViewport { // (undocumented) setActors(actors: Array): void; // (undocumented) - setCamera(cameraInterface: ICamera): void; + setCamera(cameraInterface: ICamera, storeAsInitialCamera?: boolean): void; // (undocumented) protected setCameraNoEvent(camera: ICamera): void; // (undocumented) + protected setInitialCamera(camera: ICamera): void; + // (undocumented) setOptions(options: ViewportInputOptions, immediate?: boolean): void; // (undocumented) setOrientationOfClippingPlanes(vtkPlanes: Array, slabThickness: number, viewPlaneNormal: Point3, focalPoint: Point3): void; // (undocumented) + setPan(pan: Point2, storeAsInitialCamera?: boolean): void; + // (undocumented) + setZoom(value: number, storeAsInitialCamera?: boolean): void; + // (undocumented) sHeight: number; // (undocumented) readonly suppressEvents: boolean; diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index feae341b08..e2a479f766 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -893,8 +893,10 @@ interface IViewport { _getCorners(bounds: Array): Array[]; getDefaultActor(): ActorEntry; getFrameOfReferenceUID: () => string; + getPan(): Point2; getRenderer(): void; getRenderingEngine(): any; + getZoom(): number; id: string; options: ViewportInputOptions; removeAllActors(): void; @@ -902,8 +904,10 @@ interface IViewport { renderingEngineId: string; reset(immediate: boolean): void; setActors(actors: Array): void; - setCamera(cameraInterface: ICamera): void; + setCamera(cameraInterface: ICamera, storeAsInitialCamera?: boolean): void; setOptions(options: ViewportInputOptions, immediate: boolean): void; + setPan(pan: Point2, storeAsInitialCamera?: boolean); + setZoom(zoom: number, storeAsInitialCamera?: boolean); sHeight: number; suppressEvents: boolean; sWidth: number; diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index a1e49df4fe..c31e43fc83 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -1011,6 +1011,9 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined; // @public (undocumented) function createVOISynchronizer(synchronizerName: string): Synchronizer; +// @public (undocumented) +function createZoomPanSynchronizer(synchronizerName: string): Synchronizer; + // @public (undocumented) export class CrosshairsTool extends AnnotationTool { constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps); @@ -2369,8 +2372,10 @@ interface IViewport { _getCorners(bounds: Array): Array[]; getDefaultActor(): ActorEntry; getFrameOfReferenceUID: () => string; + getPan(): Point2; getRenderer(): void; getRenderingEngine(): any; + getZoom(): number; id: string; options: ViewportInputOptions; removeAllActors(): void; @@ -2378,8 +2383,10 @@ interface IViewport { renderingEngineId: string; reset(immediate: boolean): void; setActors(actors: Array): void; - setCamera(cameraInterface: ICamera): void; + setCamera(cameraInterface: ICamera, storeAsInitialCamera?: boolean): void; setOptions(options: ViewportInputOptions, immediate: boolean): void; + setPan(pan: Point2, storeAsInitialCamera?: boolean); + setZoom(zoom: number, storeAsInitialCamera?: boolean); sHeight: number; suppressEvents: boolean; sWidth: number; @@ -3988,6 +3995,8 @@ export class Synchronizer { // (undocumented) destroy(): void; // (undocumented) + getOptions(viewportId: string): Record | undefined; + // (undocumented) getSourceViewports(): Array; // (undocumented) getTargetViewports(): Array; @@ -4005,6 +4014,8 @@ export class Synchronizer { removeSource(viewportInfo: Types_2.IViewportId): void; // (undocumented) removeTarget(viewportInfo: Types_2.IViewportId): void; + // (undocumented) + setOptions(viewportId: string, options?: Record): void; } declare namespace SynchronizerManager { @@ -4022,7 +4033,8 @@ export { SynchronizerManager } declare namespace synchronizers { export { createCameraPositionSynchronizer, - createVOISynchronizer + createVOISynchronizer, + createZoomPanSynchronizer } } export { synchronizers } diff --git a/packages/core/examples/programaticPanZoom/index.ts b/packages/core/examples/programaticPanZoom/index.ts new file mode 100644 index 0000000000..f2f38fddb6 --- /dev/null +++ b/packages/core/examples/programaticPanZoom/index.ts @@ -0,0 +1,162 @@ +import { + getRenderingEngine, + RenderingEngine, + Types, + Enums, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + ctVoiRange, + addButtonToToolbar, +} from '../../../../utils/demo/helpers'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { ViewportType } = Enums; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'CT_STACK'; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Programmatic Pan and Zoom with initial pan and zoom', + 'Displays an image at the top of the viewport, half off the screen, and has pan/zoom buttons.' +); + +const content = document.getElementById('content'); +const element = document.createElement('div'); +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); +// ============================= // +addButtonToToolbar({ + title: 'Set Pan (+5,0)', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + const pan = viewport.getPan(); + console.log('Current pan', JSON.stringify(pan)); + viewport.setPan([pan[0] + 5, pan[1]]); + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Set zoom * 1.05 ', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + const zoom = viewport.getZoom(); + console.log('Current zoom', zoom); + viewport.setZoom(zoom * 1.05); + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Reset Original', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + //viewport.resetCamera(); + viewport.setZoom(1); + viewport.setPan([0, 0]); + viewport.render(); + }, +}); + +// This can be used to see how the reset works +// Compare a reset before and after having done this +addButtonToToolbar({ + title: 'Set current offset/size as pan 0,0/zoom 1', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + viewport.setZoom(viewport.getZoom(), true); + }, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d1qmxk7r72ysft.cloudfront.net/dicomweb', + type: 'STACK', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create a stack viewport + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Define a stack containing a single image + const stack = [imageIds[0]]; + + // Set the stack on the viewport + await viewport.setStack(stack); + + // Set the VOI of the stack + viewport.setProperties({ voiRange: ctVoiRange }); + + // Render the image + viewport.render(); + + viewport.setZoom(0.8); + viewport.setPan([-128, 0]); + // Second one should have no affect + viewport.setPan([-128, 0]); +} + +run(); diff --git a/packages/core/src/RenderingEngine/Viewport.ts b/packages/core/src/RenderingEngine/Viewport.ts index 516a2d8568..4a151d5262 100644 --- a/packages/core/src/RenderingEngine/Viewport.ts +++ b/packages/core/src/RenderingEngine/Viewport.ts @@ -3,7 +3,7 @@ import vtkMatrixBuilder from '@kitware/vtk.js/Common/Core/MatrixBuilder'; import vtkMath from '@kitware/vtk.js/Common/Core/Math'; import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; -import { vec3, mat4 } from 'gl-matrix'; +import { vec2, vec3, mat4 } from 'gl-matrix'; import _cloneDeep from 'lodash.clonedeep'; import Events from '../enums/Events'; @@ -66,6 +66,10 @@ class Viewport implements IViewport { /** A flag representing if viewport methods should fire events or not */ readonly suppressEvents: boolean; protected hasPixelSpacing = true; + /** The camera that is initially defined on the reset for + * the relative pan/zoom + */ + protected initialCamera: ICamera; constructor(props: ViewportInput) { this.id = props.id; @@ -298,8 +302,6 @@ class Viewport implements IViewport { } actor.setUserMatrix(mat); - - this.getRenderingEngine().render(); }); this.getRenderingEngine().render(); @@ -536,9 +538,15 @@ class Viewport implements IViewport { * is reset for the current view. * @param resetPan - If true, the camera focal point is reset to the center of the volume (slice) * @param resetZoom - If true, the camera zoom is reset to the default zoom + * @param storeAsInitialCamera - If true, reset camera is stored as the initial camera (to allow differences to + * be detected for pan/zoom values) * @returns boolean */ - protected resetCamera(resetPan = true, resetZoom = true): boolean { + protected resetCamera( + resetPan = true, + resetZoom = true, + storeAsInitialCamera = true + ): boolean { const renderer = this.getRenderer(); const previousCamera = _cloneDeep(this.getCamera()); @@ -623,11 +631,7 @@ class Viewport implements IViewport { activeCamera.setViewUp(-viewUp[2], viewUp[0], viewUp[1]); } - let focalPointToSet = focalPoint; - - if (!resetPan) { - focalPointToSet = previousCamera.focalPoint; - } + const focalPointToSet = resetPan ? focalPoint : previousCamera.focalPoint; activeCamera.setFocalPoint( focalPointToSet[0], @@ -661,15 +665,19 @@ class Viewport implements IViewport { RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE ); + if (this.flipHorizontal || this.flipVertical) { + this.flip({ flipHorizontal: false, flipVertical: false }); + } + + if (storeAsInitialCamera) { + this.setInitialCamera(this.getCamera()); + } + const RESET_CAMERA_EVENT = { type: 'ResetCameraEvent', renderer, }; - if (this.flipHorizontal || this.flipVertical) { - this.flip({ flipHorizontal: false, flipVertical: false }); - } - // Here to let parallel/distributed compositing intercept // and do the right thing. renderer.invokeEvent(RESET_CAMERA_EVENT); @@ -682,6 +690,108 @@ class Viewport implements IViewport { return true; } + /** + * Sets the provided camera as the initial camera. + * This allows computing differences applied later as compared to the initial + * position, for things like zoom and pan. + * @param camera - to store as the initial value. + */ + protected setInitialCamera(camera: ICamera): void { + this.initialCamera = camera; + } + + /** + * Helper function to return the current canvas pan value. + * + * @returns a Point2 containing the current pan values + * on the canvas, + * computed from the current camera, where the initial pan + * value is [0,0]. + */ + public getPan(): Point2 { + const activeCamera = this.getVtkActiveCamera(); + const focalPoint = activeCamera.getFocalPoint() as Point3; + + const zero3 = this.canvasToWorld([0, 0]); + const initialCanvasFocal = this.worldToCanvas( + vec3.subtract(vec3.create(), this.initialCamera.focalPoint, zero3) + ); + const currentCanvasFocal = this.worldToCanvas( + vec3.subtract(vec3.create(), focalPoint, zero3) + ); + const result = ( + vec2.subtract(vec2.create(), initialCanvasFocal, currentCanvasFocal) + ); + return result; + } + + /** + * Sets the canvas pan value relative to the initial view position of 0,0 + * Modifies the camera to perform the pan. + */ + public setPan(pan: Point2, storeAsInitialCamera = false): void { + const previousCamera = this.getCamera(); + const { focalPoint, position } = previousCamera; + const zero3 = this.canvasToWorld([0, 0]); + const delta2 = vec2.subtract(vec2.create(), pan, this.getPan()); + if ( + Math.abs(delta2[0]) < 1 && + Math.abs(delta2[1]) < 1 && + !storeAsInitialCamera + ) { + return; + } + const delta = vec3.subtract( + vec3.create(), + this.canvasToWorld(delta2), + zero3 + ); + const newFocal = vec3.subtract(vec3.create(), focalPoint, delta); + const newPosition = vec3.subtract(vec3.create(), position, delta); + this.setCamera( + { + ...previousCamera, + focalPoint: newFocal as Point3, + position: newPosition as Point3, + }, + storeAsInitialCamera + ); + } + + /** + * Returns a current zoom level relative to the initial parallel scale + * originally applied to the image. That is, on initial display, + * the zoom level is 1. Computed as a function of the camera. + */ + public getZoom(): number { + const activeCamera = this.getVtkActiveCamera(); + const { parallelScale: initialParallelScale } = this.initialCamera; + return initialParallelScale / activeCamera.getParallelScale(); + } + + /** Zooms the image using parallel scale by updating the camera value. + * @param value - The relative parallel scale to apply. It is relative + * to the initial offsets value. + * @param storeAsInitialCamera - can be set to true to reset the camera + * after applying this zoom as the initial camera. A subsequent getZoom + * call will return "1", but the zoom will have been applied. + */ + public setZoom(value: number, storeAsInitialCamera = false): void { + const camera = this.getCamera(); + const { parallelScale: initialParallelScale } = this.initialCamera; + const parallelScale = initialParallelScale / value; + if (camera.parallelScale === parallelScale && !storeAsInitialCamera) { + return; + } + this.setCamera( + { + ...camera, + parallelScale, + }, + storeAsInitialCamera + ); + } + /** * Because the focalPoint is always in the centre of the viewport, * we must do planar computations if the frame (image "slice") is to be preserved. @@ -762,8 +872,13 @@ class Viewport implements IViewport { /** * Set the camera parameters * @param cameraInterface - ICamera + * @param storeAsInitialCamera - to set the provided camera as the initial one, + * used to compute differences for things like pan and zoom. */ - public setCamera(cameraInterface: ICamera): void { + public setCamera( + cameraInterface: ICamera, + storeAsInitialCamera = false + ): void { const vtkCamera = this.getVtkActiveCamera(); const previousCamera = _cloneDeep(this.getCamera()); const updatedCamera = Object.assign({}, previousCamera, cameraInterface); @@ -821,6 +936,10 @@ class Viewport implements IViewport { renderer.resetCameraClippingRange(); } + if (storeAsInitialCamera) { + this.setInitialCamera(updatedCamera); + } + this.triggerCameraModifiedEventIfNecessary( previousCamera, this.getCamera() diff --git a/packages/core/src/eventTarget.ts b/packages/core/src/eventTarget.ts index 5e160055d8..48c4b10d13 100644 --- a/packages/core/src/eventTarget.ts +++ b/packages/core/src/eventTarget.ts @@ -17,6 +17,11 @@ class CornerstoneEventTarget implements EventTarget { this.listeners[type] = []; } + // prevent multiple callbacks from firing + if (this.listeners[type].indexOf(callback) !== -1) { + return; + } + this.listeners[type].push(callback); } diff --git a/packages/core/src/types/IViewport.ts b/packages/core/src/types/IViewport.ts index d105d1d4ce..3eddbcb503 100644 --- a/packages/core/src/types/IViewport.ts +++ b/packages/core/src/types/IViewport.ts @@ -73,8 +73,16 @@ interface IViewport { getCanvas(): HTMLCanvasElement; /** returns camera object */ getCamera(): ICamera; + /** returns the parallel zoom relative to the default (eg returns 1 after reset) */ + getZoom(): number; + /** Sets the relative zoom - set to 1 to reset it */ + setZoom(zoom: number, storeAsInitialCamera?: boolean); + /** Gets the canvas pan value */ + getPan(): Point2; + /** Sets the canvas pan value */ + setPan(pan: Point2, storeAsInitialCamera?: boolean); /** sets the camera */ - setCamera(cameraInterface: ICamera): void; + setCamera(cameraInterface: ICamera, storeAsInitialCamera?: boolean): void; /** whether the viewport has custom rendering */ customRenderViewportToCanvas: () => unknown; _getCorners(bounds: Array): Array[]; diff --git a/packages/tools/src/store/SynchronizerManager/Synchronizer.ts b/packages/tools/src/store/SynchronizerManager/Synchronizer.ts index 69efd9e451..e65dcd6faf 100644 --- a/packages/tools/src/store/SynchronizerManager/Synchronizer.ts +++ b/packages/tools/src/store/SynchronizerManager/Synchronizer.ts @@ -21,7 +21,7 @@ class Synchronizer { private _ignoreFiredEvents: boolean; private _sourceViewports: Array; private _targetViewports: Array; - // + private _viewportOptions: Record> = {}; public id: string; constructor( @@ -48,6 +48,24 @@ class Synchronizer { return !this._enabled || !this._hasSourceElements(); } + /** + * Sets the options for the viewport id. This can be used to + * provide configuration on a viewport basis for things like offsets + * to the general synchronization, or turn on/off synchronization of certain + * attributes. + */ + public setOptions( + viewportId: string, + options: Record = {} + ): void { + this._viewportOptions[viewportId] = options; + } + + /** Gets the options for the given viewport id */ + public getOptions(viewportId: string): Record | undefined { + return this._viewportOptions[viewportId]; + } + /** * Add a viewport to the list of targets and sources both. * @param viewportInfo - The viewportId and its renderingEngineId to add to the list of targets and sources. diff --git a/packages/tools/src/synchronizers/callbacks/cameraSyncCallback.ts b/packages/tools/src/synchronizers/callbacks/cameraSyncCallback.ts index e13daa85ca..c5966bf31f 100644 --- a/packages/tools/src/synchronizers/callbacks/cameraSyncCallback.ts +++ b/packages/tools/src/synchronizers/callbacks/cameraSyncCallback.ts @@ -1,27 +1,22 @@ import { getRenderingEngine, Types } from '@cornerstonejs/core'; +import { Synchronizer } from '../../store'; /** - * Synchronizer callback to synchronize the camera. Synchronization + * Synchronizer callback to synchronize the camera by updating all camera + * values. See also zoomPanSyncCallback * * @param synchronizerInstance - The Instance of the Synchronizer * @param sourceViewport - The list of IDs defining the source viewport. - * @param targetViewport - The list of IDs defining the target viewport. + * @param targetViewport - The list of IDs defining the target viewport, never + * the same as sourceViewport. * @param cameraModifiedEvent - The CAMERA_MODIFIED event. */ export default function cameraSyncCallback( - synchronizerInstance, + synchronizerInstance: Synchronizer, sourceViewport: Types.IViewportId, targetViewport: Types.IViewportId, cameraModifiedEvent: CustomEvent ): void { - // We need a helper for this - if ( - sourceViewport.renderingEngineId === targetViewport.renderingEngineId && - sourceViewport.viewportId === targetViewport.viewportId - ) { - return; - } - const { camera } = cameraModifiedEvent.detail; const renderingEngine = getRenderingEngine(targetViewport.renderingEngineId); @@ -33,10 +28,6 @@ export default function cameraSyncCallback( const tViewport = renderingEngine.getViewport(targetViewport.viewportId); - // TODO: only sync in-plane movements if one viewport is a stack viewport - - // Todo: we shouldn't set camera, we should set the focalPoint - // to the nearest slice center world position tViewport.setCamera(camera); tViewport.render(); } diff --git a/packages/tools/src/synchronizers/callbacks/zoomPanSyncCallback.ts b/packages/tools/src/synchronizers/callbacks/zoomPanSyncCallback.ts new file mode 100644 index 0000000000..91e977a5a9 --- /dev/null +++ b/packages/tools/src/synchronizers/callbacks/zoomPanSyncCallback.ts @@ -0,0 +1,43 @@ +import { getRenderingEngine, Types } from '@cornerstonejs/core'; +import { Synchronizer } from '../../store'; + +/** + * Synchronizer callback to synchronize the camera. Synchronization + * + * targetViewport.options.syncZoom set to false to not sync the zoom + * targetViewport.options.syncPan set to false to not sync the pan + + * @param synchronizerInstance - The Instance of the Synchronizer + * @param sourceViewport - The list of IDs defining the source viewport. + * @param targetViewport - The list of IDs defining the target viewport, different + * from sourceViewport + */ +export default function zoomPanSyncCallback( + synchronizerInstance: Synchronizer, + sourceViewport: Types.IViewportId, + targetViewport: Types.IViewportId +): void { + const renderingEngine = getRenderingEngine(targetViewport.renderingEngineId); + if (!renderingEngine) { + throw new Error( + `No RenderingEngine for Id: ${targetViewport.renderingEngineId}` + ); + } + + const options = synchronizerInstance.getOptions(targetViewport.viewportId); + + const tViewport = renderingEngine.getViewport(targetViewport.viewportId); + const sViewport = renderingEngine.getViewport(sourceViewport.viewportId); + + if (options?.syncZoom !== false) { + const srcZoom = sViewport.getZoom(); + // Do the zoom first, as the pan is relative to the zoom level + tViewport.setZoom(srcZoom); + } + if (options?.syncPan !== false) { + const srcPan = sViewport.getPan(); + tViewport.setPan(srcPan); + } + + tViewport.render(); +} diff --git a/packages/tools/src/synchronizers/index.ts b/packages/tools/src/synchronizers/index.ts index 06f2a39284..d9e0b103b5 100644 --- a/packages/tools/src/synchronizers/index.ts +++ b/packages/tools/src/synchronizers/index.ts @@ -1,4 +1,9 @@ import createCameraPositionSynchronizer from './synchronizers/createCameraPositionSynchronizer'; import createVOISynchronizer from './synchronizers/createVOISynchronizer'; +import createZoomPanSynchronizer from './synchronizers/createZoomPanSynchronizer'; -export { createCameraPositionSynchronizer, createVOISynchronizer }; +export { + createCameraPositionSynchronizer, + createVOISynchronizer, + createZoomPanSynchronizer, +}; diff --git a/packages/tools/src/synchronizers/synchronizers/createCameraPositionSynchronizer.ts b/packages/tools/src/synchronizers/synchronizers/createCameraPositionSynchronizer.ts index 0a0cf947d8..b2bf781972 100644 --- a/packages/tools/src/synchronizers/synchronizers/createCameraPositionSynchronizer.ts +++ b/packages/tools/src/synchronizers/synchronizers/createCameraPositionSynchronizer.ts @@ -10,7 +10,6 @@ const { CAMERA_MODIFIED } = Enums.Events; * rendering event and calls the `cameraSyncCallback`. * * @param synchronizerName - The name of the synchronizer. - * * @returns A new `Synchronizer` instance. */ export default function createCameraPositionSynchronizer( diff --git a/packages/tools/src/synchronizers/synchronizers/createZoomPanSynchronizer.ts b/packages/tools/src/synchronizers/synchronizers/createZoomPanSynchronizer.ts new file mode 100644 index 0000000000..8a86c7dfdb --- /dev/null +++ b/packages/tools/src/synchronizers/synchronizers/createZoomPanSynchronizer.ts @@ -0,0 +1,25 @@ +import { createSynchronizer } from '../../store/SynchronizerManager'; +import { Enums } from '@cornerstonejs/core'; +import zoomPanSyncCallback from '../callbacks/zoomPanSyncCallback'; +import Synchronizer from '../../store/SynchronizerManager/Synchronizer'; + +const { CAMERA_MODIFIED } = Enums.Events; + +/** + * A helper that creates a new `Synchronizer` which listens to the `CAMERA_MODIFIED` + * rendering event and calls the `cameraSyncCallback`. + * + * @param synchronizerName - The name of the synchronizer. + * @returns A new `Synchronizer` instance. + */ +export default function createZoomPanSynchronizer( + synchronizerName: string +): Synchronizer { + const zoomPanSynchronizer = createSynchronizer( + synchronizerName, + CAMERA_MODIFIED, + zoomPanSyncCallback + ); + + return zoomPanSynchronizer; +} diff --git a/packages/tools/src/synchronizers/synchronizers/index.ts b/packages/tools/src/synchronizers/synchronizers/index.ts index 0e59a51e4b..faf6d76a6c 100644 --- a/packages/tools/src/synchronizers/synchronizers/index.ts +++ b/packages/tools/src/synchronizers/synchronizers/index.ts @@ -1,4 +1,9 @@ import createCameraPositionSynchronizer from './createCameraPositionSynchronizer'; import createVOISynchronizer from './createVOISynchronizer'; +import createZoomPanSynchronizer from './createZoomPanSynchronizer'; -export { createCameraPositionSynchronizer, createVOISynchronizer }; +export { + createCameraPositionSynchronizer, + createVOISynchronizer, + createZoomPanSynchronizer, +};