From 9539f6c56e2bd3b06f4c6b40fd6b4478d806bee3 Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 31 Oct 2022 18:14:29 -0400 Subject: [PATCH] feat: reset to center option for reset camera (#269) * fix: reset camera refactor to apply via setCamera * initial implementation for resetToCenter * fix: stack viewport reset camera * feat: convert to gl matrix * feat: add to examples * fix build * add review comments and fix test --- common/reviews/api/core.api.md | 13 +- .../api/streaming-image-volume-loader.api.md | 7 +- common/reviews/api/tools.api.md | 7 +- .../core/src/RenderingEngine/StackViewport.ts | 6 +- packages/core/src/RenderingEngine/Viewport.ts | 125 ++++++--- .../src/RenderingEngine/VolumeViewport.ts | 8 +- packages/core/src/types/ICamera.ts | 3 + packages/core/src/types/IVolumeViewport.ts | 6 +- packages/core/src/utilities/hasNaNValues.ts | 12 + packages/tools/examples/resetCamera/index.ts | 251 ++++++++++++++++++ packages/tools/test/segmentationState_test.js | 10 +- utils/ExampleRunner/example-info.json | 4 + 12 files changed, 403 insertions(+), 49 deletions(-) create mode 100644 packages/core/src/utilities/hasNaNValues.ts create mode 100644 packages/tools/examples/resetCamera/index.ts diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index c69f50ede..093ad55f6 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -650,6 +650,8 @@ interface ICachedVolume { // @public (undocumented) interface ICamera { + // (undocumented) + clippingRange?: Point2; // (undocumented) flipHorizontal?: boolean; // (undocumented) @@ -1396,7 +1398,7 @@ interface IVolumeViewport extends IViewport { // (undocumented) removeVolumeActors(actorUIDs: Array, immediate?: boolean): void; // (undocumented) - resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + resetCamera(resetPan?: boolean, resetZoom?: boolean, resetToCenter?: boolean): boolean; // (undocumented) setBlendMode(blendMode: BlendModes, filterActorUIDs?: Array, immediate?: boolean): void; // (undocumented) @@ -1991,6 +1993,11 @@ export class Viewport implements IViewport { // (undocumented) _getEdges(bounds: Array): Array<[number[], number[]]>; // (undocumented) + _getFocalPointForResetCamera(centeredFocalPoint: Point3, previousCamera: ICamera, { resetPan, resetToCenter }: { + resetPan: boolean; + resetToCenter: boolean; + }): Point3; + // (undocumented) getFrameOfReferenceUID: () => string; // (undocumented) getPan(): Point2; @@ -2027,7 +2034,7 @@ export class Viewport implements IViewport { // (undocumented) reset(immediate?: boolean): void; // (undocumented) - protected resetCamera(resetPan?: boolean, resetZoom?: boolean, storeAsInitialCamera?: boolean): boolean; + protected resetCamera(resetPan?: boolean, resetZoom?: boolean, resetToCenter?: boolean, storeAsInitialCamera?: boolean): boolean; // (undocumented) protected resetCameraNoEvent(): void; // (undocumented) @@ -2213,7 +2220,7 @@ export class VolumeViewport extends Viewport implements IVolumeViewport { // (undocumented) removeVolumeActors(actorUIDs: Array, immediate?: boolean): void; // (undocumented) - resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + resetCamera(resetPan?: boolean, resetZoom?: boolean, resetToCenter?: boolean): boolean; // (undocumented) setBlendMode(blendMode: BlendModes, filterActorUIDs?: any[], immediate?: boolean): void; // (undocumented) diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index 31a56a048..8355aed59 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -502,6 +502,7 @@ interface ICachedVolume { // @public interface ICamera { + clippingRange?: Point2; flipHorizontal?: boolean; flipVertical?: boolean; focalPoint?: Point3; @@ -1001,7 +1002,11 @@ interface IVolumeViewport extends IViewport { hasImageURI: (imageURI: string) => boolean; hasVolumeId: (volumeId: string) => boolean; removeVolumeActors(actorUIDs: Array, immediate?: boolean): void; - resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + resetCamera( + resetPan?: boolean, + resetZoom?: boolean, + resetToCenter?: boolean + ): boolean; setBlendMode( blendMode: BlendModes, filterActorUIDs?: Array, diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 67c27c023..8ab1ecbcc 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -1905,6 +1905,7 @@ interface ICachedVolume { // @public interface ICamera { + clippingRange?: Point2; flipHorizontal?: boolean; flipVertical?: boolean; focalPoint?: Point3; @@ -2529,7 +2530,11 @@ interface IVolumeViewport extends IViewport { hasImageURI: (imageURI: string) => boolean; hasVolumeId: (volumeId: string) => boolean; removeVolumeActors(actorUIDs: Array, immediate?: boolean): void; - resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + resetCamera( + resetPan?: boolean, + resetZoom?: boolean, + resetToCenter?: boolean + ): boolean; setBlendMode( blendMode: BlendModes, filterActorUIDs?: Array, diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index dac1b43f0..3c21be9e0 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -1875,8 +1875,10 @@ class StackViewport extends Viewport implements IStackViewport { // without this this.getVtkActiveCamera().roll(this.rotationCache); - // reset other properties - return super.resetCamera(resetPan, resetZoom); + // For stack Viewport we since we have only one slice + // it should be enough to reset the camera to the center of the image + const resetToCenter = true; + return super.resetCamera(resetPan, resetZoom, resetToCenter); } /** diff --git a/packages/core/src/RenderingEngine/Viewport.ts b/packages/core/src/RenderingEngine/Viewport.ts index bf450a5b7..5ca231709 100644 --- a/packages/core/src/RenderingEngine/Viewport.ts +++ b/packages/core/src/RenderingEngine/Viewport.ts @@ -10,6 +10,7 @@ import Events from '../enums/Events'; import ViewportType from '../enums/ViewportType'; import renderingEngineCache from './renderingEngineCache'; import { triggerEvent, planar, isImageActor } from '../utilities'; +import hasNaNValues from '../utilities/hasNaNValues'; import { RENDERING_DEFAULTS } from '../constants'; import type { ICamera, @@ -545,6 +546,7 @@ class Viewport implements IViewport { protected resetCamera( resetPan = true, resetZoom = true, + resetToCenter = true, storeAsInitialCamera = true ): boolean { const renderer = this.getRenderer(); @@ -573,7 +575,6 @@ class Viewport implements IViewport { // Reset the perspective zoom factors, otherwise subsequent zooms will cause // the view angle to become very small and cause bad depth sorting. // todo: parallel projection only - activeCamera.setViewAngle(90.0); focalPoint[0] = (bounds[0] + bounds[1]) / 2.0; focalPoint[1] = (bounds[2] + bounds[3]) / 2.0; @@ -624,53 +625,53 @@ class Viewport implements IViewport { radius = Math.sqrt(radius) * 0.5; const distance = 1.1 * radius; - // const distance = radius / Math.sin(angle * 0.5) - // check view-up vector against view plane normal - if (Math.abs(vtkMath.dot(viewUp, viewPlaneNormal)) > 0.999) { - activeCamera.setViewUp(-viewUp[2], viewUp[0], viewUp[1]); - } - - const focalPointToSet = resetPan ? focalPoint : previousCamera.focalPoint; + const viewUpToSet: Point3 = + Math.abs(vtkMath.dot(viewUp, viewPlaneNormal)) > 0.999 + ? [-viewUp[2], viewUp[0], viewUp[1]] + : viewUp; - activeCamera.setFocalPoint( - focalPointToSet[0], - focalPointToSet[1], - focalPointToSet[2] + const focalPointToSet = this._getFocalPointForResetCamera( + focalPoint, + previousCamera, + { resetPan, resetToCenter } ); - activeCamera.setPosition( + + const positionToSet: Point3 = [ focalPointToSet[0] + distance * viewPlaneNormal[0], focalPointToSet[1] + distance * viewPlaneNormal[1], - focalPointToSet[2] + distance * viewPlaneNormal[2] - ); + focalPointToSet[2] + distance * viewPlaneNormal[2], + ]; renderer.resetCameraClippingRange(bounds); - if (resetZoom) { - activeCamera.setParallelScale(parallelScale); - } + const clippingRangeToUse: Point2 = [ + -RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, + RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, + ]; - // update reasonable world to physical values activeCamera.setPhysicalScale(radius); - - // TODO: The PhysicalXXX stuff are used for VR only, do we need this? activeCamera.setPhysicalTranslation( -focalPointToSet[0], -focalPointToSet[1], -focalPointToSet[2] ); - activeCamera.setClippingRange( - -RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, - RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE - ); + this.setCamera({ + parallelScale: resetZoom ? parallelScale : previousCamera.parallelScale, + focalPoint: focalPointToSet, + position: positionToSet, + viewAngle: 90, + viewUp: viewUpToSet, + clippingRange: clippingRangeToUse, + flipHorizontal: this.flipHorizontal ? false : undefined, + flipVertical: this.flipVertical ? false : undefined, + }); - if (this.flipHorizontal || this.flipVertical) { - this.flip({ flipHorizontal: false, flipVertical: false }); - } + const modifiedCamera = _cloneDeep(this.getCamera()); if (storeAsInitialCamera) { - this.setInitialCamera(this.getCamera()); + this.setInitialCamera(modifiedCamera); } const RESET_CAMERA_EVENT = { @@ -682,10 +683,7 @@ class Viewport implements IViewport { // and do the right thing. renderer.invokeEvent(RESET_CAMERA_EVENT); - this.triggerCameraModifiedEventIfNecessary( - previousCamera, - this.getCamera() - ); + this.triggerCameraModifiedEventIfNecessary(previousCamera, modifiedCamera); return true; } @@ -891,6 +889,7 @@ class Viewport implements IViewport { viewAngle, flipHorizontal, flipVertical, + clippingRange, } = cameraInterface; if (flipHorizontal !== undefined || flipVertical !== undefined) { @@ -925,6 +924,10 @@ class Viewport implements IViewport { vtkCamera.setViewAngle(viewAngle); } + if (clippingRange !== undefined) { + vtkCamera.setClippingRange(clippingRange); + } + // update clippingPlanes if volume viewports const actorEntry = this.getDefaultActor(); if (actorEntry?.actor?.isA('vtkVolume')) { @@ -1101,6 +1104,62 @@ class Viewport implements IViewport { ]; } + _getFocalPointForResetCamera( + centeredFocalPoint: Point3, + previousCamera: ICamera, + { resetPan, resetToCenter }: { resetPan: boolean; resetToCenter: boolean } + ): Point3 { + if (resetToCenter && resetPan) { + return centeredFocalPoint; + } + + if (resetToCenter && !resetPan) { + return hasNaNValues(previousCamera.focalPoint) + ? centeredFocalPoint + : previousCamera.focalPoint; + } + + if (!resetToCenter && resetPan) { + // this is an interesting case that means the reset camera should not + // change the slice (default behavior is to go to the center of the + // image), and rather just reset the pan on the slice that is currently + // being viewed + const oldCamera = previousCamera; + const oldFocalPoint = oldCamera.focalPoint; + const oldViewPlaneNormal = oldCamera.viewPlaneNormal; + + const vectorFromOldFocalPointToCenteredFocalPoint = vec3.create(); + vec3.subtract( + vectorFromOldFocalPointToCenteredFocalPoint, + centeredFocalPoint, + oldFocalPoint + ); + + const distanceFromOldFocalPointToCenteredFocalPoint = vec3.dot( + vectorFromOldFocalPointToCenteredFocalPoint, + oldViewPlaneNormal + ); + + const newFocalPoint = vec3.create(); + vec3.scaleAndAdd( + newFocalPoint, + centeredFocalPoint, + oldViewPlaneNormal, + -1 * distanceFromOldFocalPointToCenteredFocalPoint + ); + + return [newFocalPoint[0], newFocalPoint[1], newFocalPoint[2]]; + } + + if (!resetPan && !resetToCenter) { + // this means the reset camera should not change the slice and should not + // touch the pan either. + return hasNaNValues(previousCamera.focalPoint) + ? centeredFocalPoint + : previousCamera.focalPoint; + } + } + /** * Determines whether or not the 3D point position is inside the boundaries of the 3D imageData. * @param point - 3D coordinate diff --git a/packages/core/src/RenderingEngine/VolumeViewport.ts b/packages/core/src/RenderingEngine/VolumeViewport.ts index f858bfebd..3ec7c0644 100644 --- a/packages/core/src/RenderingEngine/VolumeViewport.ts +++ b/packages/core/src/RenderingEngine/VolumeViewport.ts @@ -566,8 +566,12 @@ class VolumeViewport extends Viewport implements IVolumeViewport { /** * Reset the camera for the volume viewport */ - public resetCamera(resetPan = true, resetZoom = true): boolean { - super.resetCamera(resetPan, resetZoom); + public resetCamera( + resetPan = true, + resetZoom = true, + resetToCenter = true + ): boolean { + super.resetCamera(resetPan, resetZoom, resetToCenter); const activeCamera = this.getVtkActiveCamera(); // Set large numbers to ensure everything is always rendered if (activeCamera.getParallelProjection()) { diff --git a/packages/core/src/types/ICamera.ts b/packages/core/src/types/ICamera.ts index 981c0760a..fe18d9592 100644 --- a/packages/core/src/types/ICamera.ts +++ b/packages/core/src/types/ICamera.ts @@ -1,4 +1,5 @@ import Point3 from './Point3'; +import Point2 from './Point2'; /** * Camera Interface. See {@link https://kitware.github.io/vtk-examples/site/VTKBook/03Chapter3/#35-cameras} if you @@ -28,6 +29,8 @@ interface ICamera { flipHorizontal?: boolean; /** flip Vertical */ flipVertical?: boolean; + /** clipping range */ + clippingRange?: Point2; } export default ICamera; diff --git a/packages/core/src/types/IVolumeViewport.ts b/packages/core/src/types/IVolumeViewport.ts index c200123ac..e71fb5dfc 100644 --- a/packages/core/src/types/IVolumeViewport.ts +++ b/packages/core/src/types/IVolumeViewport.ts @@ -99,7 +99,11 @@ export default interface IVolumeViewport extends IViewport { /** * Reset the camera for the volume viewport */ - resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + resetCamera( + resetPan?: boolean, + resetZoom?: boolean, + resetToCenter?: boolean + ): boolean; /** * Sets the blendMode for actors of the viewport. */ diff --git a/packages/core/src/utilities/hasNaNValues.ts b/packages/core/src/utilities/hasNaNValues.ts new file mode 100644 index 000000000..8930d1f82 --- /dev/null +++ b/packages/core/src/utilities/hasNaNValues.ts @@ -0,0 +1,12 @@ +/** + * A function that checks if there is a value in the array that is NaN. + * or if the input is a number it just checks if it is NaN. + * @param input - The input to check if it is NaN. + * @returns - True if the input is NaN, false otherwise. + */ +export default function hasNaNValues(input: number[] | number): boolean { + if (Array.isArray(input)) { + return input.some((value) => Number.isNaN(value)); + } + return Number.isNaN(input); +} diff --git a/packages/tools/examples/resetCamera/index.ts b/packages/tools/examples/resetCamera/index.ts new file mode 100644 index 000000000..8b03833a1 --- /dev/null +++ b/packages/tools/examples/resetCamera/index.ts @@ -0,0 +1,251 @@ +import { + RenderingEngine, + Types, + Enums, + volumeLoader, + getRenderingEngine, +} from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, + addDropdownToToolbar, + addSliderToToolbar, + camera as cameraHelpers, + setCtTransferFunctionForVolumeActor, + addToggleButtonToToolbar, +} from '../../../../utils/demo/helpers'; + +const { + PanTool, + WindowLevelTool, + StackScrollMouseWheelTool, + ZoomTool, + ToolGroupManager, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { ViewportType } = Enums; +const { MouseBindings } = csToolsEnums; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const renderingEngineId = 'myRenderingEngine'; +const viewportIdStack = 'CT_STACK'; +const viewportIdVolume = 'CT_VOLUME'; + +// Define a unique id for the volume +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id + +// ======== Set up page ======== // +setTitleAndDescription( + 'Reset Camera API', + 'Demonstrates Different options for resetting the camera' +); + +const content = document.getElementById('content'); +const element1 = document.createElement('div'); +element1.id = 'cornerstone-element1'; +element1.style.width = '500px'; +element1.style.height = '500px'; + +const element2 = document.createElement('div'); +element2.id = 'cornerstone-element2'; +element2.style.width = '500px'; +element2.style.height = '500px'; + +element1.oncontextmenu = (e) => e.preventDefault(); +element2.oncontextmenu = (e) => e.preventDefault(); + +content.appendChild(element1); +content.appendChild(element2); +// ============================= // + +let selectedViewportId = viewportIdStack; +let resetPan = true; +let resetZoom = true; +let resetToCenter; + +addDropdownToToolbar({ + options: { + values: [viewportIdStack, viewportIdVolume], + defaultValue: viewportIdStack, + }, + onSelectedValueChange: (value) => { + selectedViewportId = value as string; + }, +}); +// Buttons +addButtonToToolbar({ + title: 'Reset Camera', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(selectedViewportId) + ); + + viewport.resetCamera(resetPan, resetZoom, resetToCenter); + + renderingEngine.render(); + }, +}); + +addToggleButtonToToolbar({ + title: 'toggle reset zoom', + onClick: (toggle) => { + resetZoom = toggle; + }, + defaultToggle: true, +}); + +addToggleButtonToToolbar({ + title: 'toggle reset pan', + onClick: (toggle) => { + resetPan = toggle; + }, + defaultToggle: true, +}); + +addToggleButtonToToolbar({ + title: 'toggle reset to center', + onClick: (toggle) => { + resetToCenter = toggle; + }, + defaultToggle: true, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(ZoomTool); + + const toolGroup = ToolGroupManager.createToolGroup('toolGroupId'); + + // Add tools to the tool group + toolGroup.addTool(WindowLevelTool.toolName); + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + + // Set the initial state of the tools, here all tools are active and bound to + // Different mouse inputs + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, // Middle Click + }, + ], + }); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, // Right Click + }, + ], + }); + // As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` + // hook instead of mouse buttons, it does not need to assign any mouse button. + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + + // 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://d3t6nz73ql33tx.cloudfront.net/dicomweb', + type: 'VOLUME', + }); + + const stackImageIds = 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://d3t6nz73ql33tx.cloudfront.net/dicomweb', + type: 'STACK', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create a stack viewport + const viewportInputs = [ + { + viewportId: viewportIdStack, + type: ViewportType.STACK, + element: element1, + defaultOptions: { + background: [1, 0.5, 0.2], + }, + }, + { + viewportId: viewportIdVolume, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0.2, 0, 0.2], + }, + }, + ]; + + renderingEngine.setViewports(viewportInputs); + + // Get the stack viewport that was created + const stackViewport = ( + renderingEngine.getViewport(viewportIdStack) + ); + + stackViewport.setStack(stackImageIds, 10); + + const volumeViewport = ( + renderingEngine.getViewport(viewportIdVolume) + ); + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + // Set the volume to load + volume.load(); + + // Set the volume on the viewport + volumeViewport.setVolumes([ + { volumeId, callback: setCtTransferFunctionForVolumeActor }, + ]); + + toolGroup.addViewport(viewportIdVolume, renderingEngineId); + toolGroup.addViewport(viewportIdStack, renderingEngineId); + + // Render the image + renderingEngine.render(); +} + +run(); diff --git a/packages/tools/test/segmentationState_test.js b/packages/tools/test/segmentationState_test.js index f42f0d473..a6775ad0c 100644 --- a/packages/tools/test/segmentationState_test.js +++ b/packages/tools/test/segmentationState_test.js @@ -235,12 +235,10 @@ describe('Segmentation State -- ', () => { ); }); - eventTarget.addEventListener( - Events.SEGMENTATION_REPRESENTATION_MODIFIED, - (evt) => { - done(); - } - ); + // wait for segmentation rendered event + eventTarget.addEventListener(Events.SEGMENTATION_RENDERED, (evt) => { + done(); + }); this.segToolGroup.addViewport(vp.id, this.renderingEngine.id); diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 6d54fe9bf..e332afb60 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -102,6 +102,10 @@ "name": "Annotation Selection and Locking", "description": "Demonstrates how to toggle the Locked and Selected states for Annotations" }, + "resetCamera": { + "name": "Viewports Reset Camera", + "description": "Demonstrates various options that are available for resetting camera on viewports" + }, "annotationVisibility": { "name": "Annotation changing visibility", "description": "Demonstrates how to toggle the Visibility state for Annotations"