cornerstone3D icon indicating copy to clipboard operation
cornerstone3D copied to clipboard

[Bug] ScaleOverlayTool: Uncaught TypeError: Cannot read properties of undefined (reading 'data')

Open xbmlz opened this issue 4 weeks ago • 7 comments

Describe the Bug

ScaleOverlayTool encountered an error while performing sequence switching display

ScaleOverlayTool.js:307 
 Uncaught TypeError: Cannot read properties of undefined (reading 'data')
    at ScaleOverlayTool.renderAnnotation (ScaleOverlayTool.js:307:36)
    at handleDrawSvg (AnnotationRenderingEngine.js:93:43)
    at Array.forEach (<anonymous>)
    at AnnotationRenderingEngine.js:97:26
    at draw (draw.js:4:5)
    at AnnotationRenderingEngine._triggerRender (AnnotationRenderingEngine.js:89:9)
    at AnnotationRenderingEngine._renderFlaggedViewports (AnnotationRenderingEngine.js:17:26)

This is my test code:

import type { Types as csTypes } from '@cornerstonejs/core'
import { Enums as csEnums, eventTarget, RenderingEngine } from '@cornerstonejs/core'
import { Enums as csToolsEnums, ScaleOverlayTool, ToolGroupManager, ZoomTool } from '@cornerstonejs/tools'
import { getStudies } from '@/api/wado'
import { useStudyStore } from '@/store'

const renderingEngine = new RenderingEngine('test')
const toolGroup = ToolGroupManager.createToolGroup('test')
const viewportDimensions = new Map<string, { width: number, height: number }>()
function Viewport({ viewportId, customStyle }: { viewportId: string, customStyle: React.CSSProperties }) {
  const elementRef = useRef<HTMLDivElement>(null)
  const { getImageIdsBySeriesInstanceUid } = useStudyStore()
  function onDropHandler(e: React.DragEvent<HTMLDivElement>) {
    const seriesInstanceUid = e.dataTransfer?.getData('seriesInstanceUid')

    const viewport = renderingEngine.getViewport(viewportId)
    if (!viewport) {
      console.error('Viewport not found')
      return
    }
    const imageIds = getImageIdsBySeriesInstanceUid(seriesInstanceUid)
    if (!imageIds || imageIds.length === 0) {
      console.error('No images found for series', seriesInstanceUid)
      return
    }
    const vp = viewport as csTypes.IStackViewport
    vp.setStack(imageIds)
    vp.render()
  }

  const onResize = useCallback(
    (entries: ResizeObserverEntry[]) => {
      if (elementRef.current && entries?.length) {
        const entry = entries[0]
        const { width, height } = entry.contentRect

        const prevDimensions = viewportDimensions.get(viewportId) || { width: 0, height: 0 }

        // Check if dimensions actually changed and then only resize if they have changed
        const hasDimensionsChanged
          = prevDimensions.width !== width || prevDimensions.height !== height

        if (width > 0 && height > 0 && hasDimensionsChanged) {
          viewportDimensions.set(viewportId, { width, height })
          // Perform resize operations
          renderingEngine.resize(true, false)
        }
      }
    },
    [viewportId],
  )

  useEffect(() => {
    const element = elementRef.current
    if (!element) {
      return
    }
    const resizeObserver = new ResizeObserver(onResize)
    resizeObserver.observe(element)
    // Cleanup function
    return () => {
      resizeObserver.unobserve(element)
      resizeObserver.disconnect()
    }
  }, [onResize])

  useEffect(() => {
    const element = elementRef.current
    if (!element) {
      return
    }
    const newImageHandler = (_event: Event) => {
      toolGroup?.addViewport(viewportId)
      toolGroup?.setToolEnabled(ScaleOverlayTool.toolName)
    }
    element.addEventListener(csEnums.Events.STACK_NEW_IMAGE, newImageHandler)
    return () => {
      element.removeEventListener(csEnums.Events.STACK_NEW_IMAGE, newImageHandler)
    }
  }, [elementRef, viewportId])

  useEffect(() => {
    const element = elementRef.current
    if (!element) {
      return
    }

    const elementEnabledHandler = () => {

    }
    renderingEngine.enableElement({
      viewportId,
      element: document.getElementById(viewportId) as HTMLDivElement,
      type: csEnums.ViewportType.STACK,
    })

    return () => {
      renderingEngine.disableElement(viewportId)
      toolGroup?.removeViewports(viewportId)
      eventTarget.removeEventListener(csEnums.Events.ELEMENT_ENABLED, elementEnabledHandler)
    }
  }, [viewportId])
  return (
    <div
      id={viewportId}
      ref={elementRef}
      style={customStyle}
      className="border-border relative"
      onDragOver={e => e.preventDefault()}
      onDrop={onDropHandler}
    >
    </div>
  )
}

export default function TestPage() {
  const studyInstanceUID = '2.25.298238216209691961446416391631262236725'

  // const [viewportIds, setViewportIds] = useState(['viewport-0'])
  const [layout, setLayout] = useState<{ rows: number, cols: number }>({ rows: 1, cols: 1 })

  const setStudies = useStudyStore(state => state.setStudies)
  const studies = useStudyStore(state => state.studies)

  const running = useRef(false)

  const { rows, cols } = layout

  const viewportIds = Array.from({ length: rows * cols }, (_, i) => `viewport-${i}`)

  function onDragStartHandler(e: React.DragEvent<HTMLDivElement>, seriesInstanceUid: string) {
    e.dataTransfer?.setData('seriesInstanceUid', seriesInstanceUid)
  }

  function getStyle(idx: number) {
    const row = Math.floor(idx / cols)
    const col = idx % cols

    // 计算宽高百分比
    const widthPercent = 100 / cols
    const heightPercent = 100 / rows

    const leftPercent = col * widthPercent
    const topPercent = row * heightPercent

    const borders = {
      borderRight: col < cols - 1 ? `1px solid var(--border)` : 'none',
      borderBottom: row < rows - 1 ? `1px solid var(--border)` : 'none',
    }
    return {
      position: 'absolute',
      width: `${widthPercent}%`,
      height: `${heightPercent}%`,
      left: `${leftPercent}%`,
      top: `${topPercent}%`,
      ...borders,
    } as React.CSSProperties
  }

  useEffect(() => {
    const setup = async () => {
      if (running.current) {
        return
      }
      running.current = true
      const studies = await getStudies(studyInstanceUID)
      setStudies(studies)

      toolGroup?.addTool(ZoomTool.toolName)
      toolGroup?.addTool(ScaleOverlayTool.toolName)
      // toolGroup?.setToolEnabled(ScaleOverlayTool.toolName)
      toolGroup?.setToolActive(ZoomTool.toolName, {
        bindings: [{ mouseButton: csToolsEnums.MouseBindings.Secondary }],
      })
    }
    setup()
  }, [setStudies])

  return (
    <div className="h-full w-full flex flex-col">
      <div>
        <input type="number" value={rows} onChange={e => setLayout({ rows: Number.parseInt(e.target.value), cols: layout.cols })} />
      </div>
      <div className="flex flex-1">
        <div className="w-60 border-r border-border flex flex-col items-center gap-2">
          {studies.map(study => (
            study.seriesList.map(series => (
              <div
                draggable
                key={series.seriesInstanceUid}
                className="border border-border"
                onDragStart={e => onDragStartHandler(e, series.seriesInstanceUid)}
              >
                <img src={series.thumbnailUrl} alt={series.seriesDesc} className="w-50 h-50 object-contain" />
              </div>
            ))
          ))}
        </div>
        <div className="flex-1 flex flex-col relative">
          <div className="h-full w-full relative">
            {viewportIds.map((viewportId, idx) => (
              <Viewport key={viewportId} viewportId={viewportId} customStyle={getStyle(idx)} />
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}

https://github.com/user-attachments/assets/f8031f31-f42a-41e4-a429-16a89a9edd4f

Steps to Reproduce

  1. Select the first sequence image, which can be displayed normally
  2. Switching to the second sequence, an error occurred

I also encountered the same error when adding ScaleOverlayTool in OHIF Viewer

The current behavior

ScaleOverlayTool.js:307 Uncaught TypeError: Cannot read properties of undefined (reading 'data') at ScaleOverlayTool.renderAnnotation (ScaleOverlayTool.js:307:36) at handleDrawSvg (AnnotationRenderingEngine.js:93:43) at Array.forEach () at AnnotationRenderingEngine.js:97:26 at draw (draw.js:4:5) at AnnotationRenderingEngine._triggerRender (AnnotationRenderingEngine.js:89:9) at AnnotationRenderingEngine._renderFlaggedViewports (AnnotationRenderingEngine.js:17:26)

The expected behavior

Normal display of ScaleOverlayTool

System Information

System: OS: Windows 11 10.0.26200 CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz Memory: 15.00 GB / 31.73 GB Binaries: Node: 20.19.0 - C:\nvm4w\nodejs\node.EXE Yarn: 1.22.22 - C:\nvm4w\nodejs\yarn.CMD npm: 10.8.2 - C:\nvm4w\nodejs\npm.CMD pnpm: 10.17.0 - C:\nvm4w\nodejs\pnpm.CMD bun: 1.3.3 - C:\Users\Lenovo.bun\bin\bun.EXE Browsers: Chrome: 142.0.7444.176 Edge: Chromium (140.0.3485.54) Internet Explorer: 11.0.26100.7309

xbmlz avatar Dec 04 '25 07:12 xbmlz

CS-76

linear[bot] avatar Dec 04 '25 07:12 linear[bot]

Debugging found that when switching sequence display, viewportsWithAnnotations has the previous viewportId, but getAnnotations(this.getToolName(), viewport.element); It's empty.

see: https://github.com/cornerstonejs/cornerstone3D/blob/main/packages/tools/src/tools/ScaleOverlayTool.ts#L115C12-L115C36 https://github.com/cornerstonejs/cornerstone3D/blob/main/packages/tools/src/tools/ScaleOverlayTool.ts#L186C25-L186C78

test image:

2.25.298238216209691961446416391631262236725.zip

xbmlz avatar Dec 04 '25 08:12 xbmlz

I tested this pull request #2504 and it did not encounter any errors, but some images still cannot display the scale overlay tool. This is normal in the cornerstoneTool library, and it also fails when dynamically switching between displayed series in the same viewport

xbmlz avatar Dec 05 '25 10:12 xbmlz

Two issues were identified:

  1. Some images cannot display the scale overlay tool
  2. When switching between series displays, the scale overlay tool cannot be updated and can only display the scale overlay tool of the first series forever

https://github.com/user-attachments/assets/e851298c-552d-41b5-afd6-7b201914ae8b

xbmlz avatar Dec 05 '25 10:12 xbmlz

we need data to test this, can you provide anonymized data

sedghi avatar Dec 05 '25 13:12 sedghi

we need data to test this, can you provide anonymized data

S0001.zip

S0002.zip

  1. For S0001, The cornerstoneTool library can display normally ScaleOverlayTool. But cornerstone3D/tools library cannot display

  2. Switch between different series in the same viewport for rendering, always displaying only the ScaleOverlayTool of the first series. But cornerstoneTool library is ok

xbmlz avatar Dec 06 '25 05:12 xbmlz

This change did not fix the actual problem #2504 @jbocce

xbmlz avatar Dec 09 '25 01:12 xbmlz