diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue
index b6b81c86..2ea372c7 100644
--- a/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue
+++ b/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue
@@ -211,21 +211,23 @@ export default {
this.mousePosition.index = []
this.mousePosition.value = null
})
- document.addEventListener('mouseup', () => {
- this.sliderMouseup()
- if (this.isMip) {
- this.rotateBarMouseup()
- }
- })
- document.addEventListener('mousemove', (e) => {
- this.sliderMousemove(e)
- if (this.isMip) {
- this.rotateBarMousemove(e)
- }
- })
+ document.addEventListener('mouseup', this.handleDocumentMouseUp)
+ document.addEventListener('mousemove', this.handleDocumentMouseMove)
// console.log(cornerstoneTools)
// element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
},
+ handleDocumentMouseUp(e) {
+ this.sliderMouseup(e)
+ if (this.isMip) {
+ this.rotateBarMouseup(e)
+ }
+ },
+ handleDocumentMouseMove(e) {
+ this.sliderMousemove(e)
+ if (this.isMip) {
+ this.rotateBarMousemove(e)
+ }
+ },
stackNewImage(e) {
const { detail } = e
if (this.series) {
@@ -936,6 +938,8 @@ export default {
},
},
beforeDestroy() {
+ document.removeEventListener('mouseup', this.handleDocumentMouseUp)
+ document.removeEventListener('mousemove', this.handleDocumentMouseMove)
this.series = null
this.topFusionVolumeActor = null
},
@@ -1180,4 +1184,5 @@ export default {
cursor: move
}
}
+
diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue
index ba2696ca..6cf541c9 100644
--- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue
+++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue
@@ -185,11 +185,6 @@
@click.prevent="openFusion">
-
-
-
false,
- minimal: { enabled: true, lineLengthInPx: 10000 }
- });
+ // toolGroup.addTool(CrosshairsTool.toolName, {
+ // getReferenceLineColor: this.setFusionCrosshairsToolLineColor,
+ // getReferenceLineSlabThicknessControlsOn: () => false,
+ // minimal: { enabled: true, lineLengthInPx: 10000 }
+ // });
} else {
toolGroup.addTool(WindowLevelTool.toolName)
}
@@ -1699,8 +1699,18 @@ export default {
})
if (viewportId === 'viewport-fusion-3') {
toolGroup.addTool(VolumeRotateTool.toolName)
- toolGroup.addTool(MIPJumpToClickTool.toolName, {
- targetViewportIds: fusionViewportIds.filter((id) => id !== viewportId)
+ toolGroup.addTool(FusionJumpToPointTool.toolName, {
+ targetViewportIds: ['viewport-fusion-0', 'viewport-fusion-1', 'viewport-fusion-2', 'viewport-fusion-3', 'viewport-fusion-hidden-sag'],
+ useBrightestPoint: true,
+ jumpToTargetViewports: true,
+ dispatchEventName: 'fusion-mip-point-selected',
+ getReferenceLineColor: this.setFusionCrosshairsToolLineColor,
+ style: this.fusionCrosshairStyle,
+ referenceLinesCenterGapRadius: this.fusionCrosshairStyle.centerHoleSize,
+ minimal: {
+ enabled: true,
+ lineLengthInPx: this.fusionCrosshairStyle.lineLength,
+ },
})
toolGroup.setToolActive(VolumeRotateTool.toolName, {
@@ -1710,13 +1720,6 @@ export default {
},
]
})
- toolGroup.setToolActive(MIPJumpToClickTool.toolName, {
- bindings: [
- {
- mouseButton: MouseBindings.Primary // Left Click
- }
- ]
- })
}
toolGroup.setToolConfiguration(
@@ -2601,13 +2604,41 @@ export default {
setFusionMipJumpEnabled(enabled) {
if (!this.isFusion) return
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
- if (!toolGroup || !toolGroup.hasTool(MIPJumpToClickTool.toolName)) return
+ if (!toolGroup || !toolGroup.hasTool(FusionJumpToPointTool.toolName)) return
if (enabled) {
- toolGroup.setToolActive(MIPJumpToClickTool.toolName, {
+ toolGroup.setToolActive(FusionJumpToPointTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }]
})
+ this.dispatchFusionCenterPoint()
} else {
- toolGroup.setToolDisabled(MIPJumpToClickTool.toolName)
+ toolGroup.setToolDisabled(FusionJumpToPointTool.toolName)
+ }
+ },
+ dispatchFusionCenterPoint() {
+ const renderingEngine = getRenderingEngine(renderingEngineId)
+ if (!renderingEngine) return
+ const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
+ const instance = toolGroup?.getToolInstance?.(FusionJumpToPointTool.toolName)
+ if (!instance?.setPoint) return
+ const candidates = ['viewport-fusion-2', 'viewport-fusion-1', 'viewport-fusion-0']
+ for (const viewportId of candidates) {
+ const viewport = renderingEngine.getViewport(viewportId)
+ if (!viewport?.canvasToWorld || !viewport?.element) continue
+ const width = viewport.element.clientWidth
+ const height = viewport.element.clientHeight
+ let worldPoint = null
+ if (width && height) {
+ worldPoint = viewport.canvasToWorld([width / 2, height / 2])
+ }
+ if ((!worldPoint || worldPoint.length < 3) && viewport.getCamera) {
+ worldPoint = viewport.getCamera()?.focalPoint
+ }
+ if (!worldPoint || worldPoint.length < 3) continue
+ instance.setPoint(worldPoint, viewportId, renderingEngine.id, {
+ jumpToTargetViewports: true,
+ dispatchEvent: false,
+ })
+ return
}
},
setFusionMipRotateEnabled(enabled) {
@@ -2625,12 +2656,15 @@ export default {
// 激活工具
setToolActive(toolName) {
if (this.histogramVisible) return false
+ if (this.isFusion && toolName === CrosshairsTool.toolName) return false
const toolGroupId = this.getActiveToolGroupId()
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
if (!toolGroup) return
if (this.activeTool === toolName) {
if (toolName === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
this.setFusionMipJumpEnabled(true)
// this.setFusionMipRotateEnabled(true)
} else {
@@ -2640,7 +2674,9 @@ export default {
} else {
if (this.activeTool) {
if (this.activeTool === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
this.setFusionMipJumpEnabled(true)
// this.setFusionMipRotateEnabled(true)
} else {
@@ -2678,32 +2714,6 @@ export default {
if (!toolGroup) return
const isMip = index === 3
-
- if (this.activeTool === CrosshairsTool.toolName) {
- this.setFusionMipJumpEnabled(false)
- if (isMip) {
- if (toolGroup.hasTool(VolumeRotateTool.toolName)) {
- toolGroup.setToolActive(VolumeRotateTool.toolName, {
- bindings: [{ mouseButton: MouseBindings.Wheel }]
- })
- }
- if (toolGroup.hasTool(StackScrollTool.toolName)) {
- toolGroup.setToolDisabled(StackScrollTool.toolName)
- }
- } else {
- if (toolGroup.hasTool(StackScrollTool.toolName)) {
- toolGroup.setToolActive(StackScrollTool.toolName, {
- bindings: [{ mouseButton: MouseBindings.Wheel }]
- })
- }
- if (toolGroup.hasTool(VolumeRotateTool.toolName)) {
- toolGroup.setToolDisabled(VolumeRotateTool.toolName)
- }
- }
- return
- }
-
- this.setFusionMipJumpEnabled(isMip)
if (isMip) {
if (toolGroup.hasTool(StackScrollTool.toolName)) {
toolGroup.setToolDisabled(StackScrollTool.toolName)
@@ -2737,7 +2747,9 @@ export default {
if (!toolGroup) return
if (this.activeTool === toolName) {
if (toolName === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
} else {
toolGroup.setToolPassive(this.activeTool)
}
@@ -2745,7 +2757,9 @@ export default {
} else {
if (this.activeTool) {
if (this.activeTool === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
} else {
toolGroup.setToolPassive(this.activeTool)
}
@@ -2770,7 +2784,9 @@ export default {
if (!toolGroup) return
if (this.activeTool) {
if (this.activeTool === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
} else {
toolGroup.setToolPassive(this.activeTool)
}
@@ -2790,7 +2806,9 @@ export default {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
if (!toolGroup) return
if (this.activeTool === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
} else {
toolGroup.setToolPassive(this.activeTool)
}
@@ -2820,7 +2838,9 @@ export default {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
if (!toolGroup) return
if (this.activeTool === CrosshairsTool.toolName) {
- toolGroup.setToolDisabled(this.activeTool)
+ if (toolGroup.hasTool(this.activeTool)) {
+ toolGroup.setToolDisabled(this.activeTool)
+ }
} else {
toolGroup.setToolPassive(this.activeTool)
}
@@ -2953,6 +2973,7 @@ export default {
}
this.resetCrosshairsAnnotationsForViewports(fusionAllViewportIds)
renderingEngine.render()
+ this.dispatchFusionCenterPoint()
if (this.fusionOverlayModality === 'NM' && Number.isFinite(this.fusionOverlayDefaultUpper) && Number.isFinite(this.fusionOverlayDefaultRange)) {
this.lastUpper = null
this.hasFusionUpperInitialized = false
@@ -4189,6 +4210,9 @@ export default {
this.$refs[`viewport-1`][0].setSeriesInfo(pt)
this.$refs[`viewport-2`][0].setSeriesInfo(pt)
this.$refs[`viewport-3`][0].setSeriesInfo(pt)
+ this.$nextTick(() => {
+ this.setFusionMipJumpEnabled(true)
+ })
// this.resetAnnotation = false
return true
}
@@ -4247,6 +4271,7 @@ export default {
this.fusionOverlayDefaultRange = null
this.fusionOverlayDefaultUpper = null
}
+ this.setFusionMipJumpEnabled(true)
})
} catch (err) {
console.log(err)
diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FusionJumpToPointTool.js b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FusionJumpToPointTool.js
new file mode 100644
index 00000000..364e4684
--- /dev/null
+++ b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FusionJumpToPointTool.js
@@ -0,0 +1,522 @@
+import { getEnabledElement, getRenderingEngine, VolumeViewport } from '@cornerstonejs/core'
+import * as cornerstoneTools from '@cornerstonejs/tools'
+
+const { AnnotationDisplayTool, ToolGroupManager, utilities, annotation, drawing } = cornerstoneTools
+const { getAnnotations, addAnnotation } = annotation.state
+const { drawLine } = drawing
+
+class FusionJumpToPointTool extends AnnotationDisplayTool {
+ constructor(toolProps = {}, defaultToolProps = {
+ supportedInteractionTypes: ['Mouse', 'Touch'],
+ configuration: {
+ targetViewportIds: [],//要联动跳转的视口
+ toolGroupId: '',
+ useBrightestPoint: true,//是否走 MIP 最亮点
+ jumpToTargetViewports: true,
+ dispatchEventName: 'fusion-mip-point-selected',//兼容外部事件监听
+ getReferenceLineColor: null,
+ style: {
+ lineWidth: 2,
+ lineLength: 20,
+ centerHoleSize: 20,
+ },
+ referenceLinesCenterGapRadius: 20,
+ minimal: {
+ enabled: true,
+ lineLengthInPx: 20,
+ },
+ },
+ }) {
+ super(toolProps, defaultToolProps)
+ this.isHandleDragging = false
+ this.suppressNextClick = false
+ this.dragSourceViewportId = null
+ }
+
+ mouseClickCallback(evt) {
+ if (this.suppressNextClick) {
+ this.suppressNextClick = false
+ return
+ }
+ const { element, currentPoints } = evt.detail || {}
+ const worldPoint = currentPoints?.world
+ if (!element || !worldPoint || worldPoint.length < 3) return
+
+ const enabledElement = getEnabledElement(element)
+ const { viewport, renderingEngine } = enabledElement || {}
+ if (!viewport || !renderingEngine) return
+
+ const selectedPoint = this._resolveSelectedPoint(viewport, worldPoint)
+ if (!selectedPoint || selectedPoint.length < 3) return
+
+ this.setPoint(selectedPoint, viewport.id, renderingEngine.id)
+ }
+
+ mouseDownCallback(evt) {
+ this._tryStartDrag(evt)
+ }
+
+ preMouseDownCallback(evt) {
+ return this._tryStartDrag(evt)
+ }
+
+ mouseDragCallback(evt) {
+ if (!this.isHandleDragging) return
+ const { element, currentPoints } = evt.detail || {}
+ if (!element) return
+
+ const enabledElement = getEnabledElement(element)
+ const { viewport, renderingEngine } = enabledElement || {}
+ if (!viewport || !renderingEngine) return
+
+ let worldPoint = currentPoints?.world
+ if ((!worldPoint || worldPoint.length < 3) && currentPoints?.canvas && viewport.canvasToWorld) {
+ worldPoint = viewport.canvasToWorld(currentPoints.canvas)
+ }
+ if (!worldPoint || worldPoint.length < 3) return
+
+ const annotation = this._getViewportCrosshairAnnotation(viewport)
+ const sourceViewportId = this.dragSourceViewportId || annotation?.data?.sourceViewportId || viewport.id
+ this.setPoint(worldPoint, sourceViewportId, renderingEngine.id)
+ evt.preventDefault?.()
+ }
+
+ mouseUpCallback(evt) {
+ if (!this.isHandleDragging) return
+ this.isHandleDragging = false
+ this.dragSourceViewportId = null
+ this.suppressNextClick = true
+ evt.preventDefault?.()
+ }
+
+ getHandleNearImagePoint(element, annotation, canvasCoords, proximity) {
+ const enabledElement = getEnabledElement(element)
+ const viewport = enabledElement?.viewport
+ if (!viewport || !annotation?.data) return null
+ if (annotation.data.type !== 'fusion-jump-crosshair') return null
+ if (annotation.data.viewportId !== viewport.id) return null
+
+ const worldPoint = annotation.data?.handles?.points?.[0]
+ if (!worldPoint) return null
+
+ const pointCanvas = viewport.worldToCanvas(worldPoint)
+ if (!pointCanvas || pointCanvas.length < 2) return null
+
+ // Keep center handle hit-test generous so dragging still works after MIP rotation.
+ const threshold = Math.max(12, proximity * 2)
+ const near = Math.hypot(canvasCoords[0] - pointCanvas[0], canvasCoords[1] - pointCanvas[1]) < threshold
+ return near ? worldPoint : null
+ }
+
+ handleSelectedCallback(evt, annotation) {
+ if (!annotation?.data || annotation.data.type !== 'fusion-jump-crosshair') return
+ this.isHandleDragging = true
+ evt.preventDefault?.()
+ }
+
+ toolSelectedCallback(evt, annotation) {
+ if (!annotation?.data || annotation.data.type !== 'fusion-jump-crosshair') return
+ this.isHandleDragging = true
+ evt.preventDefault?.()
+ }
+
+ isPointNearTool(element, annotation, canvasCoords, proximity) {
+ const enabledElement = getEnabledElement(element)
+ const viewport = enabledElement?.viewport
+ if (!viewport || !annotation?.data) return false
+ if (annotation.data.type !== 'fusion-jump-crosshair') return false
+ if (annotation.data.viewportId !== viewport.id) return false
+
+ const worldPoint = annotation.data?.handles?.points?.[0]
+ if (!worldPoint) return false
+ const pointCanvas = viewport.worldToCanvas(worldPoint)
+ if (!pointCanvas || pointCanvas.length < 2) return false
+
+ const appearance = this._normalizeAppearance(annotation.data?.crosshairAppearance || {}, annotation.data?.sourceViewportId)
+ const [cx, cy] = pointCanvas
+ const halfHole = appearance.centerHoleSize / 2
+ const len = appearance.lineLength
+ const hitPad = Math.max(8, proximity)
+
+ const segments = [
+ [[cx, cy - halfHole], [cx, cy - halfHole - len]],
+ [[cx + halfHole, cy], [cx + halfHole + len, cy]],
+ [[cx, cy + halfHole], [cx, cy + halfHole + len]],
+ [[cx - halfHole, cy], [cx - halfHole - len, cy]],
+ ]
+
+ return segments.some(([a, b]) => this._distancePointToSegment(canvasCoords, a, b) <= hitPad)
+ }
+
+ _distancePointToSegment(point, start, end) {
+ const [px, py] = point
+ const [x1, y1] = start
+ const [x2, y2] = end
+ const dx = x2 - x1
+ const dy = y2 - y1
+ const lenSq = dx * dx + dy * dy
+ if (lenSq === 0) return Math.hypot(px - x1, py - y1)
+
+ let t = ((px - x1) * dx + (py - y1) * dy) / lenSq
+ t = Math.max(0, Math.min(1, t))
+ const projX = x1 + t * dx
+ const projY = y1 + t * dy
+ return Math.hypot(px - projX, py - projY)
+ }
+
+ setPoint(worldPoint, sourceViewportId, renderingEngineId, options = {}) {
+ if (!Array.isArray(worldPoint) || worldPoint.length < 3) return
+ const renderingEngine = typeof renderingEngineId === 'string'
+ ? getRenderingEngine(renderingEngineId)
+ : renderingEngineId
+ if (!renderingEngine) return
+
+ this._applyPoint({
+ renderingEngine,
+ worldPoint,
+ sourceViewportId,
+ jumpToTargetViewports: options.jumpToTargetViewports !== false,
+ dispatchEvent: options.dispatchEvent !== false,
+ })
+ }
+
+ renderAnnotation(enabledElement, svgDrawingHelper) {
+ const { viewport } = enabledElement
+ if (!viewport?.element) return false
+
+ const annotations = getAnnotations(this.getToolName(), viewport.element) || []
+ const crosshairAnnotation = annotations.find((item) =>
+ item?.data?.type === 'fusion-jump-crosshair' && item?.data?.viewportId === viewport.id
+ )
+ if (!crosshairAnnotation) return false
+
+ const worldPoint = crosshairAnnotation.data?.handles?.points?.[0]
+ if (!worldPoint) return false
+
+ const canvasPoint = viewport.worldToCanvas(worldPoint)
+ const [cx, cy] = canvasPoint || []
+ const canvasWidth = viewport.canvas?.width / (window.devicePixelRatio || 1) || 0
+ const canvasHeight = viewport.canvas?.height / (window.devicePixelRatio || 1) || 0
+ if (!Number.isFinite(cx) || !Number.isFinite(cy) || cx < 0 || cy < 0 || cx > canvasWidth || cy > canvasHeight) {
+ return false
+ }
+
+ const appearance = this._normalizeAppearance(crosshairAnnotation.data?.crosshairAppearance || {}, crosshairAnnotation.data?.sourceViewportId)
+ const halfHole = appearance.centerHoleSize / 2
+ const len = appearance.lineLength
+ const lineOptions = {
+ color: appearance.color,
+ width: appearance.lineWidth,
+ }
+ const horizontalLineOptions = {
+ color: appearance.horizontalColor || appearance.color,
+ width: appearance.lineWidth,
+ }
+ const verticalLineOptions = {
+ color: appearance.verticalColor || appearance.color,
+ width: appearance.lineWidth,
+ }
+ const uid = crosshairAnnotation.annotationUID
+
+ drawLine(svgDrawingHelper, uid, 'seg-top', [cx, cy - halfHole], [cx, cy - halfHole - len], verticalLineOptions, `${uid}-top`)
+ drawLine(svgDrawingHelper, uid, 'seg-right', [cx + halfHole, cy], [cx + halfHole + len, cy], horizontalLineOptions, `${uid}-right`)
+ drawLine(svgDrawingHelper, uid, 'seg-bottom', [cx, cy + halfHole], [cx, cy + halfHole + len], verticalLineOptions, `${uid}-bottom`)
+ drawLine(svgDrawingHelper, uid, 'seg-left', [cx - halfHole, cy], [cx - halfHole - len, cy], horizontalLineOptions, `${uid}-left`)
+
+ return true
+ }
+
+ _applyPoint({ renderingEngine, worldPoint, sourceViewportId, jumpToTargetViewports, dispatchEvent }) {
+ const targetViewports = this._getTargetViewports(renderingEngine)
+ const sourceViewport = sourceViewportId ? renderingEngine.getViewport(sourceViewportId) : null
+ const viewportMap = new Map()
+ targetViewports.forEach((vp) => viewportMap.set(vp.id, vp))
+ if (sourceViewport) {
+ viewportMap.set(sourceViewport.id, sourceViewport)
+ }
+ const targetViewportIds = Array.from(viewportMap.keys())
+ const sourceAppearance = this._resolveCrosshairAppearance(sourceViewportId || targetViewportIds[0])
+
+ targetViewportIds.forEach((viewportId) => {
+ const viewport = viewportMap.get(viewportId)
+ if (!viewport?.element) return
+
+ if (
+ jumpToTargetViewports &&
+ this.configuration.jumpToTargetViewports &&
+ sourceViewportId &&
+ viewport.id !== sourceViewportId &&
+ viewport instanceof VolumeViewport
+ ) {
+ try {
+ viewport.jumpToWorld(worldPoint)
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ // Align with Crosshairs color semantics: resolve per target viewport id.
+ const appearance = this._resolveCrosshairAppearance(viewport.id)
+ const axisColors = this._resolveAxisColorsForViewport(viewport, renderingEngine, appearance.color)
+ this._upsertCrosshairAnnotation(viewport, worldPoint, sourceViewportId || viewport.id, appearance)
+ if (axisColors) {
+ const annotation = this._getViewportCrosshairAnnotation(viewport)
+ if (annotation?.data?.crosshairAppearance) {
+ annotation.data.crosshairAppearance.horizontalColor = axisColors.horizontalColor
+ annotation.data.crosshairAppearance.verticalColor = axisColors.verticalColor
+ }
+ }
+ viewport.render?.()
+ })
+
+ if (utilities?.triggerAnnotationRenderForViewportIds && targetViewportIds.length) {
+ utilities.triggerAnnotationRenderForViewportIds(targetViewportIds)
+ }
+
+ if (dispatchEvent) {
+ this._dispatchPointEvent(worldPoint, sourceViewportId, sourceAppearance)
+ }
+ }
+
+ _upsertCrosshairAnnotation(viewport, worldPoint, sourceViewportId, crosshairAppearance) {
+ let crosshairAnnotation = this._getViewportCrosshairAnnotation(viewport)
+
+ if (!crosshairAnnotation) {
+ const camera = viewport.getCamera?.() || {}
+ crosshairAnnotation = {
+ metadata: {
+ toolName: this.getToolName(),
+ viewPlaneNormal: camera.viewPlaneNormal ? [...camera.viewPlaneNormal] : undefined,
+ viewUp: camera.viewUp ? [...camera.viewUp] : undefined,
+ FrameOfReferenceUID: viewport.getFrameOfReferenceUID?.(),
+ referencedImageId: null,
+ },
+ data: {
+ type: 'fusion-jump-crosshair',
+ viewportId: viewport.id,
+ sourceViewportId,
+ crosshairAppearance,
+ handles: {
+ points: [[...worldPoint]],
+ },
+ },
+ }
+ addAnnotation(crosshairAnnotation, viewport.element)
+ return
+ }
+
+ crosshairAnnotation.data.sourceViewportId = sourceViewportId
+ crosshairAnnotation.data.crosshairAppearance = crosshairAppearance
+ crosshairAnnotation.data.handles.points[0] = [...worldPoint]
+ crosshairAnnotation.invalidated = true
+ }
+
+ _getViewportCrosshairAnnotation(viewport) {
+ if (!viewport?.element) return null
+ const annotations = getAnnotations(this.getToolName(), viewport.element) || []
+ return annotations.find((item) =>
+ item?.data?.type === 'fusion-jump-crosshair' && item?.data?.viewportId === viewport.id
+ ) || null
+ }
+
+ _tryStartDrag(evt) {
+ const { element, currentPoints } = evt.detail || {}
+ const canvasPoint = currentPoints?.canvas
+ if (!element || !canvasPoint) return false
+
+ const enabledElement = getEnabledElement(element)
+ const viewport = enabledElement?.viewport
+ if (!viewport?.element) return false
+
+ const annotation = this._getViewportCrosshairAnnotation(viewport)
+ if (!annotation) return false
+
+ const nearHandle = this.getHandleNearImagePoint(element, annotation, canvasPoint, 10)
+ const nearLine = this.isPointNearTool(element, annotation, canvasPoint, 10)
+ this.isHandleDragging = !!(nearHandle || nearLine)
+ if (!this.isHandleDragging) return false
+
+ this.dragSourceViewportId = annotation?.data?.sourceViewportId || viewport.id
+ evt.preventDefault?.()
+ return true
+ }
+
+ _dispatchPointEvent(worldPoint, sourceViewportId, crosshairAppearance) {
+ const { dispatchEventName } = this.configuration
+ if (!dispatchEventName || typeof window === 'undefined') return
+ window.dispatchEvent(new CustomEvent(dispatchEventName, {
+ detail: {
+ world: [...worldPoint],
+ sourceViewportId,
+ crosshairAppearance,
+ }
+ }))
+ }
+
+ _resolveSelectedPoint(viewport, worldPoint) {
+ if (!this.configuration.useBrightestPoint) {
+ return worldPoint
+ }
+
+ const volumeId = viewport.getVolumeId?.()
+ if (!volumeId) {
+ return worldPoint
+ }
+
+ let maxIntensity = -Infinity
+ const maxFn = (intensity, point) => {
+ if (intensity > maxIntensity) {
+ maxIntensity = intensity
+ return point
+ }
+ }
+
+ const brightestPoint = utilities.planar.getPointInLineOfSightWithCriteria(
+ viewport,
+ worldPoint,
+ volumeId,
+ maxFn
+ )
+
+ return brightestPoint && brightestPoint.length >= 3 ? brightestPoint : worldPoint
+ }
+
+ _getTargetViewports(renderingEngine) {
+ const { targetViewportIds, toolGroupId } = this.configuration
+ const hasTargetIds = Array.isArray(targetViewportIds) && targetViewportIds.length > 0
+
+ return renderingEngine.getViewports().filter((vp) => {
+ if (hasTargetIds && targetViewportIds.includes(vp.id)) {
+ return true
+ }
+
+ if (toolGroupId) {
+ const toolGroup = ToolGroupManager.getToolGroupForViewport(vp.id, renderingEngine.id)
+ if (toolGroup?.id === toolGroupId) {
+ return true
+ }
+ }
+
+ return !hasTargetIds && !toolGroupId
+ })
+ }
+
+ _resolveCrosshairAppearance(sourceViewportId) {
+ const {
+ getReferenceLineColor,
+ style = {},
+ referenceLinesCenterGapRadius,
+ minimal = {},
+ } = this.configuration
+
+ const color = typeof getReferenceLineColor === 'function'
+ ? getReferenceLineColor(sourceViewportId)
+ : null
+
+ const minimalLineLength = minimal?.enabled && Number.isFinite(minimal?.lineLengthInPx)
+ ? minimal.lineLengthInPx
+ : undefined
+ const lineLength = Number.isFinite(style.lineLength)
+ ? style.lineLength
+ : (Number.isFinite(minimalLineLength) ? minimalLineLength : 40)
+ const centerHoleSize = Number.isFinite(style.centerHoleSize)
+ ? style.centerHoleSize
+ : (Number.isFinite(referenceLinesCenterGapRadius) ? referenceLinesCenterGapRadius : 20)
+
+ return this._normalizeAppearance({
+ color: color || '#6fb9ff',
+ lineWidth: Number.isFinite(style.lineWidth) ? style.lineWidth : 2,
+ lineLength,
+ centerHoleSize,
+ }, sourceViewportId)
+ }
+
+ _resolveAxisColorsForViewport(viewport, renderingEngine, fallbackColor) {
+ if (!viewport || !renderingEngine) return null
+
+ const orientation = this._getViewportOrientation(viewport)
+ const orientationColorMap = this._getOrientationColorMap(renderingEngine, fallbackColor)
+ const defaultColor = fallbackColor || '#6fb9ff'
+
+ if (orientation === 'axial') {
+ return {
+ horizontalColor: orientationColorMap.sagittal || defaultColor,
+ verticalColor: orientationColorMap.coronal || defaultColor,
+ }
+ }
+ if (orientation === 'sagittal') {
+ return {
+ horizontalColor: orientationColorMap.coronal || defaultColor,
+ verticalColor: orientationColorMap.axial || defaultColor,
+ }
+ }
+ if (orientation === 'coronal') {
+ return {
+ horizontalColor: orientationColorMap.sagittal || defaultColor,
+ verticalColor: orientationColorMap.axial || defaultColor,
+ }
+ }
+
+ return {
+ horizontalColor: defaultColor,
+ verticalColor: defaultColor,
+ }
+ }
+
+ _getOrientationColorMap(renderingEngine, fallbackColor) {
+ const map = {
+ axial: null,
+ sagittal: null,
+ coronal: null,
+ }
+ const targetViewports = this._getTargetViewports(renderingEngine)
+ targetViewports.forEach((vp) => {
+ const orientation = this._getViewportOrientation(vp)
+ if (!orientation || map[orientation]) return
+ map[orientation] = this._getReferenceLineColor(vp.id, fallbackColor)
+ })
+ return map
+ }
+
+ _getViewportOrientation(viewport) {
+ const normal = viewport?.getCamera?.()?.viewPlaneNormal
+ if (!normal || normal.length < 3) return null
+ const abs = normal.map((n) => Math.abs(n))
+ const max = Math.max(abs[0], abs[1], abs[2])
+ if (max === abs[2]) return 'axial'
+ if (max === abs[0]) return 'sagittal'
+ if (max === abs[1]) return 'coronal'
+ return null
+ }
+
+ _getReferenceLineColor(viewportId, fallbackColor) {
+ if (typeof this.configuration.getReferenceLineColor === 'function') {
+ const color = this.configuration.getReferenceLineColor(viewportId)
+ if (color) return color
+ }
+ return fallbackColor || '#6fb9ff'
+ }
+
+ _normalizeAppearance(appearance = {}, sourceViewportId) {
+ const lineWidth = Number.isFinite(appearance.lineWidth) ? appearance.lineWidth : 2
+ const lineLength = Number.isFinite(appearance.lineLength) ? appearance.lineLength : 9
+ const centerHoleSize = Number.isFinite(appearance.centerHoleSize) ? appearance.centerHoleSize : 8
+ let color = appearance.color
+ if (!color && typeof this.configuration.getReferenceLineColor === 'function') {
+ color = this.configuration.getReferenceLineColor(sourceViewportId)
+ }
+
+ return {
+ color: color || '#6fb9ff',
+ lineWidth: Math.max(1, lineWidth),
+ lineLength: Math.max(4, lineLength),
+ centerHoleSize: Math.max(2, centerHoleSize),
+ horizontalColor: appearance.horizontalColor || null,
+ verticalColor: appearance.verticalColor || null,
+ }
+ }
+}
+
+FusionJumpToPointTool.toolName = 'FusionJumpToPointTool'
+
+export default FusionJumpToPointTool