diff --git a/bun.lock b/bun.lock index f941050fa..b318cadcd 100644 --- a/bun.lock +++ b/bun.lock @@ -41,7 +41,7 @@ }, "addOns/externals/devDependencies": { "name": "@externals/devDependencies", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@kitware/vtk.js": "32.12.0", @@ -126,14 +126,14 @@ }, "addOns/externals/dicom-microscopy-viewer": { "name": "@externals/dicom-microscopy-viewer", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "dicom-microscopy-viewer": "^0.48.6", }, }, "extensions/cornerstone": { "name": "@ohif/extension-cornerstone", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/adapters": "^3.32.12", @@ -159,8 +159,8 @@ "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/dicom-image-loader": "^3.32.12", - "@ohif/core": "3.12.0-beta.7", - "@ohif/ui": "3.12.0-beta.7", + "@ohif/core": "3.12.0-beta.8", + "@ohif/ui": "3.12.0-beta.8", "dcmjs": "0.43.1", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -172,7 +172,7 @@ }, "extensions/cornerstone-dicom-pmap": { "name": "@ohif/extension-cornerstone-dicom-pmap", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/adapters": "^3.32.12", @@ -195,7 +195,7 @@ }, "extensions/cornerstone-dicom-rt": { "name": "@ohif/extension-cornerstone-dicom-rt", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "react-color": "^2.19.3", @@ -215,7 +215,7 @@ }, "extensions/cornerstone-dicom-seg": { "name": "@ohif/extension-cornerstone-dicom-seg", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/adapters": "^3.32.12", @@ -238,7 +238,7 @@ }, "extensions/cornerstone-dicom-sr": { "name": "@ohif/extension-cornerstone-dicom-sr", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/adapters": "^3.32.12", @@ -247,10 +247,10 @@ "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.7", - "@ohif/extension-cornerstone": "3.12.0-beta.7", - "@ohif/extension-measurement-tracking": "3.12.0-beta.7", - "@ohif/ui": "3.12.0-beta.7", + "@ohif/core": "3.12.0-beta.8", + "@ohif/extension-cornerstone": "3.12.0-beta.8", + "@ohif/extension-measurement-tracking": "3.12.0-beta.8", + "@ohif/ui": "3.12.0-beta.8", "dcmjs": "0.43.1", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -260,7 +260,7 @@ }, "extensions/cornerstone-dynamic-volume": { "name": "@ohif/extension-cornerstone-dynamic-volume", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/core": "^3.32.12", @@ -282,7 +282,7 @@ }, "extensions/default": { "name": "@ohif/extension-default", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/calculate-suv": "^1.1.0", @@ -290,8 +290,8 @@ "lodash.uniqby": "^4.7.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.7", - "@ohif/i18n": "3.12.0-beta.7", + "@ohif/core": "3.12.0-beta.8", + "@ohif/i18n": "3.12.0-beta.8", "dcmjs": "0.43.1", "dicomweb-client": "^0.10.4", "prop-types": "^15.6.2", @@ -305,7 +305,7 @@ }, "extensions/dicom-microscopy": { "name": "@ohif/extension-dicom-microscopy", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", @@ -330,7 +330,7 @@ }, "extensions/dicom-pdf": { "name": "@ohif/extension-dicom-pdf", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", @@ -347,7 +347,7 @@ }, "extensions/dicom-video": { "name": "@ohif/extension-dicom-video", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", @@ -364,20 +364,20 @@ }, "extensions/measurement-tracking": { "name": "@ohif/extension-measurement-tracking", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", - "@ohif/ui": "3.12.0-beta.7", + "@ohif/ui": "3.12.0-beta.8", "@xstate/react": "^3.2.2", "xstate": "^4.10.0", }, "peerDependencies": { "@cornerstonejs/core": "^3.32.12", "@cornerstonejs/tools": "^3.32.12", - "@ohif/core": "3.12.0-beta.7", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.7", - "@ohif/extension-default": "3.12.0-beta.7", - "@ohif/ui": "3.12.0-beta.7", + "@ohif/core": "3.12.0-beta.8", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.8", + "@ohif/extension-default": "3.12.0-beta.8", + "@ohif/ui": "3.12.0-beta.8", "classnames": "^2.3.2", "dcmjs": "0.43.1", "lodash.debounce": "^4.0.8", @@ -390,7 +390,7 @@ }, "extensions/test-extension": { "name": "@ohif/extension-test", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", @@ -407,7 +407,7 @@ }, "extensions/tmtv": { "name": "@ohif/extension-tmtv", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", @@ -424,7 +424,7 @@ }, "extensions/usAnnotation": { "name": "@ohif/extension-ultrasound-pleura-bline", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/core": "^3.32.12", @@ -476,7 +476,7 @@ }, "modes/basic-dev-mode": { "name": "@ohif/mode-basic-dev-mode", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -496,7 +496,7 @@ }, "modes/basic-test-mode": { "name": "@ohif/mode-test", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -518,7 +518,7 @@ }, "modes/longitudinal": { "name": "@ohif/mode-longitudinal", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -541,7 +541,7 @@ }, "modes/microscopy": { "name": "@ohif/mode-microscopy", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -553,7 +553,7 @@ }, "modes/preclinical-4d": { "name": "@ohif/mode-preclinical-4d", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", }, @@ -572,7 +572,7 @@ }, "modes/segmentation": { "name": "@ohif/mode-segmentation", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -616,7 +616,7 @@ }, "modes/tmtv": { "name": "@ohif/mode-tmtv", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -637,7 +637,7 @@ }, "modes/usAnnotation": { "name": "@ohif/mode-ultrasound-pleura-bline", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/core": "^3.32.12", @@ -676,7 +676,7 @@ }, "platform/app": { "name": "@ohif/app", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", @@ -744,7 +744,7 @@ }, "platform/cli": { "name": "@ohif/cli", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "bin": { "ohif-cli": "src/index.js", }, @@ -768,7 +768,7 @@ }, "platform/core": { "name": "@ohif/core", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "dcmjs": "0.43.1", @@ -795,14 +795,14 @@ "@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/core": "^3.32.12", "@cornerstonejs/dicom-image-loader": "^3.32.12", - "@ohif/ui": "3.12.0-beta.7", + "@ohif/ui": "3.12.0-beta.8", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21", }, }, "platform/i18n": { "name": "@ohif/i18n", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@babel/runtime": "^7.20.13", "i18next-locize-backend": "^2.0.0", @@ -827,7 +827,7 @@ }, "platform/ui": { "name": "@ohif/ui", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@testing-library/react": "^13.1.0", "browser-detect": "^0.2.28", @@ -887,7 +887,7 @@ }, "platform/ui-next": { "name": "@ohif/ui-next", - "version": "3.12.0-beta.7", + "version": "3.12.0-beta.8", "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 9671a4d43..35de213f9 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -40,6 +40,8 @@ import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport'; import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils'; import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { isMeasurementWithinViewport } from './utils/isMeasurementWithinViewport'; +import { getCenterExtent } from './utils/getCenterExtent'; const { DefaultHistoryMemo } = csUtils.HistoryMemo; const toggleSyncFunctions = { @@ -161,6 +163,27 @@ function commandsModule({ const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); viewport.setViewReference(metadata); viewport.render(); + + /** + * If the measurement is not visible inside the current viewport, + * we need to move the camera to the measurement. + */ + if (!isMeasurementWithinViewport(viewport, measurement)) { + const camera = viewport.getCamera(); + const { focalPoint: cameraFocalPoint, position: cameraPosition } = camera; + const { center, extent } = getCenterExtent(measurement); + const position = vec3.sub(vec3.create(), cameraPosition, cameraFocalPoint); + vec3.add(position, position, center); + viewport.setCamera({ focalPoint: center, position: position as any }); + /** Zoom out if the measurement is too large */ + const measurementSize = vec3.dist(extent.min, extent.max); + if (measurementSize > camera.parallelScale) { + const scaleFactor = measurementSize / camera.parallelScale; + viewport.setZoom(viewport.getZoom() / scaleFactor); + } + viewport.render(); + } + return; } diff --git a/extensions/cornerstone/src/utils/getCenterExtent.ts b/extensions/cornerstone/src/utils/getCenterExtent.ts new file mode 100644 index 000000000..8873ec1ab --- /dev/null +++ b/extensions/cornerstone/src/utils/getCenterExtent.ts @@ -0,0 +1,46 @@ +/** + * Calculates the center point and bounding box extent of a measurement based on its points. + * @param {Object} measurement - The measurement object containing points + * @param {Array>} measurement.points - Array of 3D points [x, y, z] + * @returns {Object} Object containing center and extent + * @returns {Array} returns.center - The center point [x, y, z] + * @returns {Object} returns.extent - The bounding box extent with min and max points + */ +export const getCenterExtent = (measurement: { points?: number[][] }) => { + const { points } = measurement; + + if (!points || !Array.isArray(points) || points.length === 0) { + // Return default values if no points are available + const defaultCenter: [number, number, number] = [0, 0, 0]; + const defaultExtent = { + min: [0, 0, 0] as [number, number, number], + max: [0, 0, 0] as [number, number, number], + }; + return { center: defaultCenter, extent: defaultExtent }; + } + + // Initialize min and max with the first point + const min: [number, number, number] = [...points[0]] as [number, number, number]; + const max: [number, number, number] = [...points[0]] as [number, number, number]; + + // Find the bounding box by iterating through all points + for (let i = 1; i < points.length; i++) { + const point = points[i]; + for (let j = 0; j < 3; j++) { + min[j] = Math.min(min[j], point[j]); + max[j] = Math.max(max[j], point[j]); + } + } + + // Calculate the center point + const center: [number, number, number] = [ + (min[0] + max[0]) / 2, + (min[1] + max[1]) / 2, + (min[2] + max[2]) / 2, + ]; + + return { + center, + extent: { min, max }, + }; +}; diff --git a/extensions/cornerstone/src/utils/index.ts b/extensions/cornerstone/src/utils/index.ts index a05c5509c..6e0224d15 100644 --- a/extensions/cornerstone/src/utils/index.ts +++ b/extensions/cornerstone/src/utils/index.ts @@ -9,6 +9,7 @@ import promptHydrationDialog, { HydrationCallback, HydrationSRResult, } from './promptHydrationDialog'; +import { getCenterExtent } from './getCenterExtent'; const utils = { handleSegmentChange, @@ -16,6 +17,7 @@ const utils = { setupSegmentationDataModifiedHandler, setupSegmentationModifiedHandler, promptHydrationDialog, + getCenterExtent, }; export type { HydrationDialogProps, HydrationCallback, HydrationSRResult }; diff --git a/extensions/cornerstone/src/utils/isMeasurementWithinViewport.ts b/extensions/cornerstone/src/utils/isMeasurementWithinViewport.ts new file mode 100644 index 000000000..dc18aec4d --- /dev/null +++ b/extensions/cornerstone/src/utils/isMeasurementWithinViewport.ts @@ -0,0 +1,33 @@ +import { getCenterExtent } from './getCenterExtent'; + +/** + * Determines if a measurement is within the current viewport extent. + * Uses the measurement's bounding box extent to efficiently check if the entire + * measurement fits within the viewport's visible area. + * + * @param {Object} viewport - The viewport object containing camera information + * @param {Object} measurement - The measurement object containing points to check + * @returns {boolean} True if the measurement extent is within the viewport, false otherwise + */ +export const isMeasurementWithinViewport = (viewport, measurement) => { + const camera = viewport.getCamera(); + const { focalPoint, parallelScale } = camera; + + // Get the measurement's bounding box extent + const { extent } = getCenterExtent(measurement); + const { min, max } = extent; + + // Check if the entire bounding box fits within the viewport extent + // We need to check both the min and max corners of the bounding box + for (let i = 0; i < 3; i++) { + const minDistance = Math.abs(min[i] - focalPoint[i]); + const maxDistance = Math.abs(max[i] - focalPoint[i]); + + // If either the min or max point is outside the viewport extent, return false + if (minDistance > parallelScale || maxDistance > parallelScale) { + return false; + } + } + + return true; // The entire measurement extent is within the viewport +}; diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png b/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png index b4a06f2d0..49925b01e 100644 Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png differ