融合视口新增定位工具
continuous-integration/drone/push Build is passing Details

uat_us
caiyiling 2026-04-27 11:49:07 +08:00
parent 91386589ae
commit ff0d204612
3 changed files with 621 additions and 69 deletions

View File

@ -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
}
}
</style>

View File

@ -185,11 +185,6 @@
@click.prevent="openFusion">
<svg-icon icon-class="fusion" class="svg-icon" />
</div>
<div :class="['tool-item', activeTool === 'Crosshairs' ? 'tool-item-active' : '']"
v-if="readingTool === 2 && isFusion" :title="$t('trials:reading:button:crosshairs')"
@click.prevent="setToolActive('Crosshairs')">
<svg-icon icon-class="crosshairs" class="svg-icon" />
</div>
<div v-for="tool in tools" :key="tool.toolName"
:class="['tool-item', readingTaskState === 2 ? 'tool-disabled' : '', activeTool === tool.toolName ? 'tool-item-active' : '']"
:style="{ cursor: tool.isDisabled ? 'not-allowed' : 'pointer' }"
@ -582,6 +577,7 @@ import colorMap from './colorMap.vue'
import RectangleROITool from './tools/RectangleROITool'
import ScaleOverlayTool from './tools/ScaleOverlayTool'
import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool'
import FusionJumpToPointTool from './tools/FusionJumpToPointTool'
import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache'
import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool'
import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom'
@ -615,7 +611,6 @@ const {
AngleTool,
CobbAngleTool,
EraserTool,
MIPJumpToClickTool,
VolumeRotateTool,
CrosshairsTool,
EllipticalROITool,
@ -824,6 +819,11 @@ export default {
fusionOverlayModality: null,
fusionOverlayDefaultUpper: null,
fusionOverlayDefaultRange: null,
fusionCrosshairStyle: {
lineWidth: 2,
lineLength: 20,
centerHoleSize: 20,
},
lastUpper: null,
hasFusionUpperInitialized: false,
timer: {},
@ -1530,7 +1530,7 @@ export default {
cornerstoneTools.addTool(EllipticalROITool)
cornerstoneTools.addTool(AngleTool)
cornerstoneTools.addTool(CobbAngleTool)
cornerstoneTools.addTool(MIPJumpToClickTool)
cornerstoneTools.addTool(FusionJumpToPointTool)
cornerstoneTools.addTool(VolumeRotateTool)
cornerstoneTools.addTool(CrosshairsTool)
cornerstoneTools.addTool(LabelMapEditWithContourTool)
@ -1641,11 +1641,11 @@ export default {
getReferenceLineColor: this.setCrosshairsToolLineColor
});
} else if (toolGroupId === this.fusionToolGroupId) {
toolGroup.addTool(CrosshairsTool.toolName, {
getReferenceLineColor: this.setFusionCrosshairsToolLineColor,
getReferenceLineSlabThicknessControlsOn: () => 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)
}
@ -2952,6 +2972,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
@ -4188,6 +4209,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
}
@ -4246,6 +4270,7 @@ export default {
this.fusionOverlayDefaultRange = null
this.fusionOverlayDefaultUpper = null
}
this.setFusionMipJumpEnabled(true)
})
} catch (err) {
console.log(err)

View File

@ -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