融合视口新增定位工具
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
91386589ae
commit
ff0d204612
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue