[Bug] ScaleOverlayTool: Uncaught TypeError: Cannot read properties of undefined (reading 'data')
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
- Select the first sequence image, which can be displayed normally
- 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 (
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
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:
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
Two issues were identified:
- Some images cannot display the scale overlay tool
- 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
we need data to test this, can you provide anonymized data
we need data to test this, can you provide anonymized data
-
For S0001, The
cornerstoneToollibrary can display normally ScaleOverlayTool. Butcornerstone3D/toolslibrary cannot display -
Switch between different series in the same viewport for rendering, always displaying only the
ScaleOverlayToolof the first series. ButcornerstoneToollibrary is ok
This change did not fix the actual problem #2504 @jbocce