From 4fcdbc252f761e2bbbd4821f5945b4d9df2c592f Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 11 May 2022 23:15:46 -0400 Subject: [PATCH 1/5] feat: Add CINE tool via playClip --- common/reviews/api/tools.api.md | 31 ++- packages/tools/src/utilities/cine/events.ts | 8 + packages/tools/src/utilities/cine/index.ts | 5 + packages/tools/src/utilities/cine/playClip.ts | 246 ++++++++++++++++++ packages/tools/src/utilities/cine/state.ts | 31 +++ packages/tools/src/utilities/index.ts | 2 + 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 packages/tools/src/utilities/cine/events.ts create mode 100644 packages/tools/src/utilities/cine/index.ts create mode 100644 packages/tools/src/utilities/cine/playClip.ts create mode 100644 packages/tools/src/utilities/cine/state.ts diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 120457b08e..30231ad74b 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -51,6 +51,9 @@ function addSegmentations(segmentationInputArray: SegmentationPublicInput[]): vo // @public (undocumented) export function addTool(ToolClass: any): void; +// @public (undocumented) +function addToolState(element: HTMLDivElement, data: CINEToolData): void; + // @public (undocumented) interface AngleAnnotation extends Annotation { // (undocumented) @@ -552,6 +555,16 @@ export function cancelActiveManipulations(element: HTMLDivElement): string | und // @public (undocumented) function checkAndDefineIsLockedProperty(annotation: Annotation): void; +declare namespace cine { + export { + playClip, + stopClip, + Events_2 as Events, + getToolState, + addToolState + } +} + // @public (undocumented) export class CircleScissorsTool extends BaseTool { constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps); @@ -1375,6 +1388,12 @@ enum Events { SEGMENTATION_REPRESENTATION_REMOVED = "CORNERSTONE_TOOLS_SEGMENTATION_REPRESENTATION_REMOVED" } +// @public (undocumented) +enum Events_2 { + // (undocumented) + CLIP_STOPPED = "CORNERSTONE_CINE_TOOL_STOPPED" +} + declare namespace EventTypes { export { CameraModifiedEventDetail, @@ -1642,6 +1661,9 @@ function getToolGroupSpecificConfig_2(toolGroupId: string): SegmentationRepresen // @public (undocumented) function getToolGroupsWithSegmentation(segmentationId: string): string[]; +// @public (undocumented) +function getToolState(element: HTMLDivElement): CINEToolData | undefined; + // @public (undocumented) function getViewportIdsWithToolToRender(element: HTMLDivElement, toolName: string, requireSameOrientation?: boolean): string[]; @@ -2794,6 +2816,9 @@ export class PlanarFreehandROITool extends AnnotationTool { // @public type Plane = [number, number, number, number]; +// @public (undocumented) +function playClip(element: HTMLDivElement, framesPerSecond: number): void; + // @public type Point2 = [number, number]; @@ -3622,6 +3647,9 @@ declare namespace state_2 { } } +// @public (undocumented) +function stopClip(element: HTMLDivElement): void; + // @public (undocumented) type StyleConfig = { annotations?: { @@ -3925,7 +3953,8 @@ declare namespace utilities { pointInSurroundingSphereCallback, getAnnotationNearPoint, getAnnotationNearPointOnEnabledElement, - jumpToSlice + jumpToSlice, + cine } } export { utilities } diff --git a/packages/tools/src/utilities/cine/events.ts b/packages/tools/src/utilities/cine/events.ts new file mode 100644 index 0000000000..a188185b68 --- /dev/null +++ b/packages/tools/src/utilities/cine/events.ts @@ -0,0 +1,8 @@ +/** + * CINE Tool Events + */ +enum Events { + CLIP_STOPPED = 'CORNERSTONE_CINE_TOOL_STOPPED', +} + +export default Events; diff --git a/packages/tools/src/utilities/cine/index.ts b/packages/tools/src/utilities/cine/index.ts new file mode 100644 index 0000000000..263e8e8152 --- /dev/null +++ b/packages/tools/src/utilities/cine/index.ts @@ -0,0 +1,5 @@ +import { playClip, stopClip } from './playClip'; +import Events from './events'; +import { getToolState, addToolState } from './state'; + +export { playClip, stopClip, Events, getToolState, addToolState }; diff --git a/packages/tools/src/utilities/cine/playClip.ts b/packages/tools/src/utilities/cine/playClip.ts new file mode 100644 index 0000000000..91f5617d68 --- /dev/null +++ b/packages/tools/src/utilities/cine/playClip.ts @@ -0,0 +1,246 @@ +import { + utilities, + getEnabledElement, + StackViewport, +} from '@cornerstonejs/core'; +import CINE_EVENTS from './events'; +import { addToolState, getToolState } from './state'; + +const { triggerEvent } = utilities; + +/** + * Starts playing a clip or adjusts the frame rate of an already playing clip. framesPerSecond is + * optional and defaults to 30 if not specified. A negative framesPerSecond will play the clip in reverse. + * The element must be a stack of images + * @param element - HTML Element + * @param framesPerSecond - Number of frames per second + */ +function playClip(element: HTMLDivElement, framesPerSecond: number): void { + let playClipData; + let playClipTimeouts; + + if (element === undefined) { + throw new Error('playClip: element must not be undefined'); + } + + const enabledElement = getEnabledElement(element); + + if (!enabledElement) { + throw new Error( + 'playClip: element must be a valid Cornerstone enabled element' + ); + } + + const { viewport } = enabledElement; + + if (!(viewport instanceof StackViewport)) { + throw new Error( + 'playClip: element must be a stack viewport, volume viewport playClip not yet implemented' + ); + } + + const stackData = { + currentImageIdIndex: viewport.getCurrentImageIdIndex(), + imageIds: viewport.getImageIds(), + }; + + const playClipToolData = getToolState(element); + + if (!playClipToolData) { + playClipData = { + intervalId: undefined, + framesPerSecond: 30, + lastFrameTimeStamp: undefined, + frameRate: 0, + frameTimeVector: undefined, + ignoreFrameTimeVector: false, + usingFrameTimeVector: false, + speed: 1, + reverse: false, + loop: true, + }; + addToolState(element, playClipData); + } else { + playClipData = playClipToolData; + // Make sure the specified clip is not running before any property update + _stopClipWithData(playClipData); + } + + // If a framesPerSecond is specified and is valid, update the playClipData now + if (framesPerSecond < 0 || framesPerSecond > 0) { + playClipData.framesPerSecond = Number(framesPerSecond); + playClipData.reverse = playClipData.framesPerSecond < 0; + // If framesPerSecond is given, frameTimeVector will be ignored... + playClipData.ignoreFrameTimeVector = true; + } + + // Determine if frame time vector should be used instead of a fixed frame rate... + if ( + playClipData.ignoreFrameTimeVector !== true && + playClipData.frameTimeVector && + playClipData.frameTimeVector.length === stackData.imageIds.length + ) { + playClipTimeouts = _getPlayClipTimeouts( + playClipData.frameTimeVector, + playClipData.speed + ); + } + + // This function encapsulates the frame rendering logic... + const playClipAction = () => { + // Hoisting of context variables + let newImageIdIndex = stackData.currentImageIdIndex; + + const imageCount = stackData.imageIds.length; + + if (playClipData.reverse) { + newImageIdIndex--; + } else { + newImageIdIndex++; + } + + if ( + !playClipData.loop && + (newImageIdIndex < 0 || newImageIdIndex >= imageCount) + ) { + _stopClipWithData(playClipData); + const eventDetail = { + element, + }; + + triggerEvent(element, CINE_EVENTS.CLIP_STOPPED, eventDetail); + + return; + } + + // Loop around if we go outside the stack + if (newImageIdIndex >= imageCount) { + newImageIdIndex = 0; + } + + if (newImageIdIndex < 0) { + newImageIdIndex = imageCount - 1; + } + + if (newImageIdIndex !== stackData.currentImageIdIndex) { + viewport.setImageIdIndex(newImageIdIndex).then(() => { + stackData.currentImageIdIndex = newImageIdIndex; + }); + } + }; + + // If playClipTimeouts array is available, not empty and its elements are NOT uniform ... + // ... (at least one timeout is different from the others), use alternate setTimeout implementation + if ( + playClipTimeouts && + playClipTimeouts.length > 0 && + playClipTimeouts.isTimeVarying + ) { + playClipData.usingFrameTimeVector = true; + playClipData.intervalId = setTimeout(function playClipTimeoutHandler() { + playClipData.intervalId = setTimeout( + playClipTimeoutHandler, + playClipTimeouts[stackData.currentImageIdIndex] + ); + playClipAction(); + }, 0); + } else { + // ... otherwise user setInterval implementation which is much more efficient. + playClipData.usingFrameTimeVector = false; + playClipData.intervalId = setInterval( + playClipAction, + 1000 / Math.abs(playClipData.framesPerSecond) + ); + } +} + +/** + * Stops an already playing clip. + * @param element - HTML Element + */ +function stopClip(element: HTMLDivElement): void { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + const cineToolData = getToolState(viewport.element); + + if (!cineToolData) { + return; + } + + _stopClipWithData(cineToolData); +} + +/** + * [private] Turns a Frame Time Vector (0018,1065) array into a normalized array of timeouts. Each element + * ... of the resulting array represents the amount of time each frame will remain on the screen. + * @param vector - A Frame Time Vector (0018,1065) as specified in section C.7.6.5.1.2 of DICOM standard. + * @param speed - A speed factor which will be applied to each element of the resulting array. + * @returns An array with timeouts for each animation frame. + */ +function _getPlayClipTimeouts(vector: number[], speed: number) { + let i; + let sample; + let delay; + let sum = 0; + const limit = vector.length; + const timeouts = []; + + // Initialize time varying to false + // @ts-ignore + timeouts.isTimeVarying = false; + + if (typeof speed !== 'number' || speed <= 0) { + speed = 1; + } + + // First element of a frame time vector must be discarded + for (i = 1; i < limit; i++) { + // eslint-disable-next-line no-bitwise + delay = (Number(vector[i]) / speed) | 0; // Integral part only + timeouts.push(delay); + if (i === 1) { + // Use first item as a sample for comparison + sample = delay; + } else if (delay !== sample) { + // @ts-ignore + timeouts.isTimeVarying = true; + } + + sum += delay; + } + + if (timeouts.length > 0) { + // @ts-ignore + if (timeouts.isTimeVarying) { + // If it's a time varying vector, make the last item an average... + // eslint-disable-next-line no-bitwise + delay = (sum / timeouts.length) | 0; + } else { + delay = timeouts[0]; + } + + timeouts.push(delay); + } + + return timeouts; +} + +/** + * [private] Performs the heavy lifting of stopping an ongoing animation. + * @param playClipData - The data from playClip that needs to be stopped. + */ +function _stopClipWithData(playClipData) { + const id = playClipData.intervalId; + + if (typeof id !== 'undefined') { + playClipData.intervalId = undefined; + if (playClipData.usingFrameTimeVector) { + clearTimeout(id); + } else { + clearInterval(id); + } + } +} + +export { playClip, stopClip }; diff --git a/packages/tools/src/utilities/cine/state.ts b/packages/tools/src/utilities/cine/state.ts new file mode 100644 index 0000000000..9400284c16 --- /dev/null +++ b/packages/tools/src/utilities/cine/state.ts @@ -0,0 +1,31 @@ +import { getEnabledElement } from '@cornerstonejs/core'; + +interface CINEToolData { + intervalId: number | undefined; + framesPerSecond: number; + lastFrameTimeStamp: number | undefined; + frameRate: number; + frameTimeVector: number[] | undefined; + ignoreFrameTimeVector: boolean; + usingFrameTimeVector: boolean; + speed: number; + reverse: boolean; + loop: boolean; + data?: unknown[]; +} + +const state: Record = {}; + +function addToolState(element: HTMLDivElement, data: CINEToolData): void { + const enabledElement = getEnabledElement(element); + const { viewportId } = enabledElement; + state[viewportId] = data; +} + +function getToolState(element: HTMLDivElement): CINEToolData | undefined { + const enabledElement = getEnabledElement(element); + const { viewportId } = enabledElement; + return state[viewportId]; +} + +export { addToolState, getToolState }; diff --git a/packages/tools/src/utilities/index.ts b/packages/tools/src/utilities/index.ts index c21264a735..794ef6a1bb 100644 --- a/packages/tools/src/utilities/index.ts +++ b/packages/tools/src/utilities/index.ts @@ -23,6 +23,7 @@ import * as planar from './planar'; import * as stackScrollTool from './stackScrollTool'; import * as viewportFilters from './viewportFilters'; import * as orientation from './orientation'; +import * as cine from './cine'; // Events import { triggerEvent } from '@cornerstonejs/core'; @@ -47,4 +48,5 @@ export { getAnnotationNearPoint, getAnnotationNearPointOnEnabledElement, jumpToSlice, + cine, }; From 7573858b17b8f0340f152cd0d20d20de108621bc Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 12 May 2022 09:33:04 -0400 Subject: [PATCH 2/5] apply review comments --- common/reviews/api/tools.api.md | 48 ++++- packages/tools/examples/CINETool/index.ts | 179 ++++++++++++++++++ packages/tools/src/types/CINETypes.ts | 20 ++ packages/tools/src/types/index.ts | 3 + packages/tools/src/utilities/cine/events.ts | 1 + packages/tools/src/utilities/cine/playClip.ts | 64 ++++--- packages/tools/src/utilities/cine/state.ts | 21 +- utils/ExampleRunner/example-info.json | 12 +- 8 files changed, 298 insertions(+), 50 deletions(-) create mode 100644 packages/tools/examples/CINETool/index.ts create mode 100644 packages/tools/src/types/CINETypes.ts diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 30231ad74b..37ec8a04b5 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -52,7 +52,7 @@ function addSegmentations(segmentationInputArray: SegmentationPublicInput[]): vo export function addTool(ToolClass: any): void; // @public (undocumented) -function addToolState(element: HTMLDivElement, data: CINEToolData): void; +function addToolState(element: HTMLDivElement, data: CINETypes.ToolData): void; // @public (undocumented) interface AngleAnnotation extends Annotation { @@ -565,6 +565,13 @@ declare namespace cine { } } +declare namespace CINETypes { + export { + PlayClipOptions, + ToolData + } +} + // @public (undocumented) export class CircleScissorsTool extends BaseTool { constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps); @@ -1390,6 +1397,8 @@ enum Events { // @public (undocumented) enum Events_2 { + // (undocumented) + CLIP_STARTED = "CORNERSTONE_CINE_TOOL_STARTED", // (undocumented) CLIP_STOPPED = "CORNERSTONE_CINE_TOOL_STOPPED" } @@ -1662,7 +1671,7 @@ function getToolGroupSpecificConfig_2(toolGroupId: string): SegmentationRepresen function getToolGroupsWithSegmentation(segmentationId: string): string[]; // @public (undocumented) -function getToolState(element: HTMLDivElement): CINEToolData | undefined; +function getToolState(element: HTMLDivElement): CINETypes.ToolData | undefined; // @public (undocumented) function getViewportIdsWithToolToRender(element: HTMLDivElement, toolName: string, requireSameOrientation?: boolean): string[]; @@ -2817,7 +2826,15 @@ export class PlanarFreehandROITool extends AnnotationTool { type Plane = [number, number, number, number]; // @public (undocumented) -function playClip(element: HTMLDivElement, framesPerSecond: number): void; +function playClip(element: HTMLDivElement, playClipOptions: CINETypes.PlayClipOptions): void; + +// @public (undocumented) +type PlayClipOptions = { + framesPerSecond?: number; + frameTimeVector?: number[]; + reverse?: boolean; + loop?: boolean; +}; // @public type Point2 = [number, number]; @@ -3764,6 +3781,28 @@ function throttle(func: Function, wait?: number, options?: { trailing?: boolean; }): Function; +// @public (undocumented) +interface ToolData { + // (undocumented) + framesPerSecond: number; + // (undocumented) + frameTimeVector: number[] | undefined; + // (undocumented) + ignoreFrameTimeVector: boolean; + // (undocumented) + intervalId: number | undefined; + // (undocumented) + lastFrameTimeStamp: number | undefined; + // (undocumented) + loop: boolean; + // (undocumented) + reverse: boolean; + // (undocumented) + speed: number; + // (undocumented) + usingFrameTimeVector: boolean; +} + declare namespace ToolGroupManager { export { createToolGroup, @@ -3925,7 +3964,8 @@ declare namespace Types { LabelmapTypes, SVGCursorDescriptor, SVGPoint_2 as SVGPoint, - ScrollOptions_2 as ScrollOptions + ScrollOptions_2 as ScrollOptions, + CINETypes } } export { Types } diff --git a/packages/tools/examples/CINETool/index.ts b/packages/tools/examples/CINETool/index.ts new file mode 100644 index 0000000000..6712879eaa --- /dev/null +++ b/packages/tools/examples/CINETool/index.ts @@ -0,0 +1,179 @@ +import { + RenderingEngine, + Types, + Enums, + utilities as csUtils, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, + addSliderToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { + WindowLevelTool, + PanTool, + ZoomTool, + ToolGroupManager, + Enums: csToolsEnums, + utilities: csToolsUtilities, +} = cornerstoneTools; + +const { ViewportType } = Enums; +const { MouseBindings } = csToolsEnums; + +// ======== Set up page ======== // +setTitleAndDescription('CINE Tool', 'Show the usage of the CINE Tool.'); + +const content = document.getElementById('content'); +const element = document.createElement('div'); + +// Disable right click context menu so we can have right click tools +element.oncontextmenu = (e) => e.preventDefault(); + +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); + +const instructions = document.createElement('p'); +instructions.innerText = ` + - Click on Play Clip to start the CINE tool + - Click on Stop Clip to stop the CINE tool + - Drag the frame slider to change the frames per second rate +`; + +content.append(instructions); +// ============================= // + +const toolGroupId = 'STACK_TOOL_GROUP_ID'; +let framesPerSecond = 24; + +addButtonToToolbar({ + title: 'Play Clip', + onClick: () => { + csToolsUtilities.cine.playClip(element, { framesPerSecond }); + }, +}); + +addButtonToToolbar({ + title: 'Stop Clip', + onClick: () => { + csToolsUtilities.cine.stopClip(element); + }, +}); + +addSliderToToolbar({ + title: `Frame per second`, + range: [1, 100], + defaultValue: framesPerSecond, + onSelectedValueChange: (value) => { + csToolsUtilities.cine.stopClip(element); + framesPerSecond = Number(value); + csToolsUtilities.cine.playClip(element, { framesPerSecond }); + }, + updateLabelOnChange: (value, label) => { + label.innerText = `Frames per second: ${value}`; + }, +}); +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add the tools to the tool group + toolGroup.addTool(WindowLevelTool.toolName); + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + + // Set the initial state of the tools, here we set one tool active on left click. + // This means left click will draw that tool. + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + ], + }); + + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, + }, + ], + }); + + // 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 renderingEngineId = 'myRenderingEngine'; + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create a stack viewport + const viewportId = 'CT_STACK'; + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Set the tool group on the viewport + toolGroup.addViewport(viewportId, renderingEngineId); + + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Set the stack on the viewport + viewport.setStack(imageIds).then(() => { + csUtils.prefetchStack(imageIds); + }); + + // Render the image + viewport.render(); +} + +run(); diff --git a/packages/tools/src/types/CINETypes.ts b/packages/tools/src/types/CINETypes.ts new file mode 100644 index 0000000000..23b705e8e2 --- /dev/null +++ b/packages/tools/src/types/CINETypes.ts @@ -0,0 +1,20 @@ +type PlayClipOptions = { + framesPerSecond?: number; + frameTimeVector?: number[]; + reverse?: boolean; + loop?: boolean; +}; + +interface ToolData { + intervalId: number | undefined; + framesPerSecond: number; + lastFrameTimeStamp: number | undefined; + frameTimeVector: number[] | undefined; + ignoreFrameTimeVector: boolean; + usingFrameTimeVector: boolean; + speed: number; + reverse: boolean; + loop: boolean; +} + +export type { PlayClipOptions, ToolData }; diff --git a/packages/tools/src/types/index.ts b/packages/tools/src/types/index.ts index f82a1bdf40..0b10dff697 100644 --- a/packages/tools/src/types/index.ts +++ b/packages/tools/src/types/index.ts @@ -23,6 +23,7 @@ import type { ToolProps, PublicToolProps } from './ToolProps'; import type { SVGCursorDescriptor, SVGPoint } from './CursorTypes'; import type JumpToSliceOptions from './JumpToSliceOptions'; import type ScrollOptions from './ScrollOptions'; +import type * as CINETypes from './CINETypes'; import type { Color, ColorLUT, @@ -81,4 +82,6 @@ export type { SVGPoint, // Scroll ScrollOptions, + // CINE + CINETypes, }; diff --git a/packages/tools/src/utilities/cine/events.ts b/packages/tools/src/utilities/cine/events.ts index a188185b68..37162034d7 100644 --- a/packages/tools/src/utilities/cine/events.ts +++ b/packages/tools/src/utilities/cine/events.ts @@ -3,6 +3,7 @@ */ enum Events { CLIP_STOPPED = 'CORNERSTONE_CINE_TOOL_STOPPED', + CLIP_STARTED = 'CORNERSTONE_CINE_TOOL_STARTED', } export default Events; diff --git a/packages/tools/src/utilities/cine/playClip.ts b/packages/tools/src/utilities/cine/playClip.ts index 91f5617d68..d72d422ec1 100644 --- a/packages/tools/src/utilities/cine/playClip.ts +++ b/packages/tools/src/utilities/cine/playClip.ts @@ -5,6 +5,7 @@ import { } from '@cornerstonejs/core'; import CINE_EVENTS from './events'; import { addToolState, getToolState } from './state'; +import { CINETypes } from '../../types'; const { triggerEvent } = utilities; @@ -15,9 +16,12 @@ const { triggerEvent } = utilities; * @param element - HTML Element * @param framesPerSecond - Number of frames per second */ -function playClip(element: HTMLDivElement, framesPerSecond: number): void { - let playClipData; +function playClip( + element: HTMLDivElement, + playClipOptions: CINETypes.PlayClipOptions +): void { let playClipTimeouts; + let playClipIsTimeVarying; if (element === undefined) { throw new Error('playClip: element must not be undefined'); @@ -44,14 +48,13 @@ function playClip(element: HTMLDivElement, framesPerSecond: number): void { imageIds: viewport.getImageIds(), }; - const playClipToolData = getToolState(element); + let playClipData = getToolState(element); - if (!playClipToolData) { + if (!playClipData) { playClipData = { intervalId: undefined, framesPerSecond: 30, lastFrameTimeStamp: undefined, - frameRate: 0, frameTimeVector: undefined, ignoreFrameTimeVector: false, usingFrameTimeVector: false, @@ -61,14 +64,16 @@ function playClip(element: HTMLDivElement, framesPerSecond: number): void { }; addToolState(element, playClipData); } else { - playClipData = playClipToolData; // Make sure the specified clip is not running before any property update _stopClipWithData(playClipData); } // If a framesPerSecond is specified and is valid, update the playClipData now - if (framesPerSecond < 0 || framesPerSecond > 0) { - playClipData.framesPerSecond = Number(framesPerSecond); + if ( + playClipOptions.framesPerSecond < 0 || + playClipOptions.framesPerSecond > 0 + ) { + playClipData.framesPerSecond = Number(playClipOptions.framesPerSecond); playClipData.reverse = playClipData.framesPerSecond < 0; // If framesPerSecond is given, frameTimeVector will be ignored... playClipData.ignoreFrameTimeVector = true; @@ -80,10 +85,13 @@ function playClip(element: HTMLDivElement, framesPerSecond: number): void { playClipData.frameTimeVector && playClipData.frameTimeVector.length === stackData.imageIds.length ) { - playClipTimeouts = _getPlayClipTimeouts( + const { timeouts, isTimeVarying } = _getPlayClipTimeouts( playClipData.frameTimeVector, playClipData.speed ); + + playClipTimeouts = timeouts; + playClipIsTimeVarying = isTimeVarying; } // This function encapsulates the frame rendering logic... @@ -134,24 +142,33 @@ function playClip(element: HTMLDivElement, framesPerSecond: number): void { if ( playClipTimeouts && playClipTimeouts.length > 0 && - playClipTimeouts.isTimeVarying + playClipIsTimeVarying ) { playClipData.usingFrameTimeVector = true; - playClipData.intervalId = setTimeout(function playClipTimeoutHandler() { - playClipData.intervalId = setTimeout( - playClipTimeoutHandler, - playClipTimeouts[stackData.currentImageIdIndex] - ); - playClipAction(); - }, 0); + playClipData.intervalId = window.setTimeout( + function playClipTimeoutHandler() { + playClipData.intervalId = window.setTimeout( + playClipTimeoutHandler, + playClipTimeouts[stackData.currentImageIdIndex] + ); + playClipAction(); + }, + 0 + ); } else { // ... otherwise user setInterval implementation which is much more efficient. playClipData.usingFrameTimeVector = false; - playClipData.intervalId = setInterval( + playClipData.intervalId = window.setInterval( playClipAction, 1000 / Math.abs(playClipData.framesPerSecond) ); } + + const eventDetail = { + element, + }; + + triggerEvent(element, CINE_EVENTS.CLIP_STARTED, eventDetail); } /** @@ -187,8 +204,7 @@ function _getPlayClipTimeouts(vector: number[], speed: number) { const timeouts = []; // Initialize time varying to false - // @ts-ignore - timeouts.isTimeVarying = false; + let isTimeVarying = false; if (typeof speed !== 'number' || speed <= 0) { speed = 1; @@ -203,16 +219,14 @@ function _getPlayClipTimeouts(vector: number[], speed: number) { // Use first item as a sample for comparison sample = delay; } else if (delay !== sample) { - // @ts-ignore - timeouts.isTimeVarying = true; + isTimeVarying = true; } sum += delay; } if (timeouts.length > 0) { - // @ts-ignore - if (timeouts.isTimeVarying) { + if (isTimeVarying) { // If it's a time varying vector, make the last item an average... // eslint-disable-next-line no-bitwise delay = (sum / timeouts.length) | 0; @@ -223,7 +237,7 @@ function _getPlayClipTimeouts(vector: number[], speed: number) { timeouts.push(delay); } - return timeouts; + return { timeouts, isTimeVarying }; } /** diff --git a/packages/tools/src/utilities/cine/state.ts b/packages/tools/src/utilities/cine/state.ts index 9400284c16..5ac40abbe4 100644 --- a/packages/tools/src/utilities/cine/state.ts +++ b/packages/tools/src/utilities/cine/state.ts @@ -1,28 +1,15 @@ import { getEnabledElement } from '@cornerstonejs/core'; +import { CINETypes } from '../../types'; -interface CINEToolData { - intervalId: number | undefined; - framesPerSecond: number; - lastFrameTimeStamp: number | undefined; - frameRate: number; - frameTimeVector: number[] | undefined; - ignoreFrameTimeVector: boolean; - usingFrameTimeVector: boolean; - speed: number; - reverse: boolean; - loop: boolean; - data?: unknown[]; -} - -const state: Record = {}; +const state: Record = {}; -function addToolState(element: HTMLDivElement, data: CINEToolData): void { +function addToolState(element: HTMLDivElement, data: CINETypes.ToolData): void { const enabledElement = getEnabledElement(element); const { viewportId } = enabledElement; state[viewportId] = data; } -function getToolState(element: HTMLDivElement): CINEToolData | undefined { +function getToolState(element: HTMLDivElement): CINETypes.ToolData | undefined { const enabledElement = getEnabledElement(element); const { viewportId } = enabledElement; return state[viewportId]; diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 2581852d55..2a83653072 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -66,10 +66,6 @@ } }, "tools-basic": { - "basic": { - "name": "Basic Usage", - "description": "Demonstrates how to set up and use Tools with Cornerstone" - }, "multipleToolGroups": { "name": "Multiple Tool Groups", "description": "Demonstrates the usage of multiple tool groups for a set of viewports." @@ -98,6 +94,14 @@ "name": "Binding Tools with Modifier Keys", "description": "Demonstrates how to bind a tool to a keyboard and mouse combination (e.g. shift+click, ctrl+click)" }, + "magnifyTool": { + "name": "Magnify Tool", + "description": "Demonstrates the usage of the magnification tool" + }, + "CINETool": { + "name": "CINE Tool", + "description": "Demonstrates the usage of the CINE tool" + }, "segmentationRendering": { "name": "Labelmap Segmentation Rendering", "description": "Demonstrates how to add a Labelmap to the viewports for rendering" From 395a14ec1b8782158db60d1827784c223e18e067 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 12 May 2022 09:55:09 -0400 Subject: [PATCH 3/5] apply review comments --- packages/tools/src/types/CINETypes.ts | 1 + packages/tools/src/utilities/cine/playClip.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/tools/src/types/CINETypes.ts b/packages/tools/src/types/CINETypes.ts index 23b705e8e2..ca3e304070 100644 --- a/packages/tools/src/types/CINETypes.ts +++ b/packages/tools/src/types/CINETypes.ts @@ -3,6 +3,7 @@ type PlayClipOptions = { frameTimeVector?: number[]; reverse?: boolean; loop?: boolean; + frameTimeVectorSpeedMultiplier?: number; }; interface ToolData { diff --git a/packages/tools/src/utilities/cine/playClip.ts b/packages/tools/src/utilities/cine/playClip.ts index d72d422ec1..fe74d2ef16 100644 --- a/packages/tools/src/utilities/cine/playClip.ts +++ b/packages/tools/src/utilities/cine/playClip.ts @@ -55,12 +55,12 @@ function playClip( intervalId: undefined, framesPerSecond: 30, lastFrameTimeStamp: undefined, - frameTimeVector: undefined, ignoreFrameTimeVector: false, usingFrameTimeVector: false, - speed: 1, - reverse: false, - loop: true, + frameTimeVector: playClipOptions.frameTimeVector ?? undefined, + speed: playClipOptions.frameTimeVectorSpeedMultiplier ?? 1, + reverse: playClipOptions.reverse ?? false, + loop: playClipOptions.loop ?? false, }; addToolState(element, playClipData); } else { From 724bc84d0276925c013771d036ccaa7d0bbc5cd9 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 12 May 2022 10:12:12 -0400 Subject: [PATCH 4/5] fix api build --- common/reviews/api/tools.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 37ec8a04b5..bce337c5ef 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -2834,6 +2834,7 @@ type PlayClipOptions = { frameTimeVector?: number[]; reverse?: boolean; loop?: boolean; + frameTimeVectorSpeedMultiplier?: number; }; // @public From e12d21c8f2baf8691d535534949d0a16c552b5a2 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 12 May 2022 11:09:53 -0400 Subject: [PATCH 5/5] update default value for loop --- packages/tools/src/utilities/cine/playClip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/src/utilities/cine/playClip.ts b/packages/tools/src/utilities/cine/playClip.ts index fe74d2ef16..c4bc204eaf 100644 --- a/packages/tools/src/utilities/cine/playClip.ts +++ b/packages/tools/src/utilities/cine/playClip.ts @@ -60,7 +60,7 @@ function playClip( frameTimeVector: playClipOptions.frameTimeVector ?? undefined, speed: playClipOptions.frameTimeVectorSpeedMultiplier ?? 1, reverse: playClipOptions.reverse ?? false, - loop: playClipOptions.loop ?? false, + loop: playClipOptions.loop ?? true, }; addToolState(element, playClipData); } else {