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 15791bf2..c598008f 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) } @@ -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) 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