UNPKG

@cornerstonejs/core

Version:
369 lines (368 loc) • 13.5 kB
import renderingEngineCache from '../../renderingEngineCache.js'; import { RenderModesPanel, } from './RenderModesPanel.js'; import { StatsPanel } from './StatsPanel.js'; import { PanelType } from './enums.js'; import { STATS_CONFIG, PANEL_CONFIGS, CONVERSION } from './constants.js'; export class StatsOverlay { constructor() { this.dom = null; this.startTime = 0; this.lastUpdateTime = 0; this.frameCount = 0; this.panels = new Map(); this.metricsColumn = null; this.bindingsColumn = null; this.animationFrameId = null; this.isSetup = false; this.dragPointerId = null; this.dragOffsetX = 0; this.dragOffsetY = 0; this.handlePointerDown = (event) => this.onPointerDown(event); this.handlePointerMove = (event) => this.onPointerMove(event); this.handlePointerUp = (event) => this.onPointerUp(event); } static getInstance() { if (!StatsOverlay.instance) { StatsOverlay.instance = new StatsOverlay(); } return StatsOverlay.instance; } setup() { if (this.isSetup) { return; } try { this.dom = this.createOverlayElement(); this.startTime = performance.now(); this.lastUpdateTime = this.startTime; this.initializePanels(); this.applyOverlayStyles(); this.restorePosition(); this.attachDragHandlers(); document.body.appendChild(this.dom); this.startLoop(); this.isSetup = true; } catch (error) { console.warn('Failed to setup stats overlay:', error); } } cleanup() { this.stopLoop(); this.detachDragHandlers(); if (this.dom && this.dom.parentNode) { this.dom.parentNode.removeChild(this.dom); } this.dom = null; this.metricsColumn = null; this.bindingsColumn = null; this.panels.clear(); this.isSetup = false; } showPanel(_panelType) { } update() { this.startTime = this.updateStats(); } createOverlayElement() { return document.createElement('div'); } applyOverlayStyles() { Object.assign(this.dom.style, STATS_CONFIG.OVERLAY_STYLES); } initializePanels() { this.initializePanelColumns(); const fpsPanel = new StatsPanel(PANEL_CONFIGS[PanelType.FPS].name, PANEL_CONFIGS[PanelType.FPS].foregroundColor, PANEL_CONFIGS[PanelType.FPS].backgroundColor); this.addPanel(PanelType.FPS, fpsPanel); const msPanel = new StatsPanel(PANEL_CONFIGS[PanelType.MS].name, PANEL_CONFIGS[PanelType.MS].foregroundColor, PANEL_CONFIGS[PanelType.MS].backgroundColor); this.addPanel(PanelType.MS, msPanel); if (this.isMemoryAvailable()) { const memPanel = new StatsPanel(PANEL_CONFIGS[PanelType.MEMORY].name, PANEL_CONFIGS[PanelType.MEMORY].foregroundColor, PANEL_CONFIGS[PanelType.MEMORY].backgroundColor); this.addPanel(PanelType.MEMORY, memPanel); } this.addPanel(PanelType.RENDER_MODES, new RenderModesPanel()); } initializePanelColumns() { this.metricsColumn = document.createElement('div'); this.metricsColumn.style.cssText = ` display:flex; flex-direction:column; flex:0 0 auto; `; this.bindingsColumn = document.createElement('div'); this.bindingsColumn.style.cssText = ` display:flex; flex-direction:column; flex:0 1 auto; min-width:0; `; this.dom.appendChild(this.metricsColumn); this.dom.appendChild(this.bindingsColumn); } isMemoryAvailable() { const perf = performance; return perf.memory !== undefined; } addPanel(type, panel) { const column = type === PanelType.RENDER_MODES ? this.bindingsColumn : this.metricsColumn; (column ?? this.dom).appendChild(panel.dom); this.panels.set(type, panel); } startLoop() { const loop = () => { this.update(); this.animationFrameId = requestAnimationFrame(loop); }; this.animationFrameId = requestAnimationFrame(loop); } stopLoop() { if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } } updateStats() { this.frameCount++; const currentTime = performance.now(); const deltaTime = currentTime - this.startTime; const msPanel = this.panels.get(PanelType.MS); if (msPanel) { msPanel.update(deltaTime, STATS_CONFIG.MAX_MS_VALUE); } if (currentTime >= this.lastUpdateTime + STATS_CONFIG.UPDATE_INTERVAL) { const fps = (this.frameCount * CONVERSION.MS_PER_SECOND) / (currentTime - this.lastUpdateTime); const fpsPanel = this.panels.get(PanelType.FPS); if (fpsPanel) { fpsPanel.update(fps, STATS_CONFIG.MAX_FPS_VALUE); } this.lastUpdateTime = currentTime; this.frameCount = 0; this.updateMemoryPanel(); this.updateRenderModesPanel(); } return currentTime; } updateRenderModesPanel() { const panel = this.panels.get(PanelType.RENDER_MODES); if (!(panel instanceof RenderModesPanel)) { return; } const entries = []; for (const renderingEngine of renderingEngineCache.getAll()) { if (!renderingEngine || renderingEngine.hasBeenDestroyed) { continue; } for (const viewport of renderingEngine.getViewports()) { const debugViewport = viewport; const renderModes = debugViewport._debug?.renderModes; entries.push({ renderingEngineId: renderingEngine.id, viewportId: viewport.id, viewportType: viewport.type, bindings: this.getViewportBindingDebugEntries(debugViewport, renderModes ?? {}), }); } } panel.setContent(entries); } getViewportBindingDebugEntries(viewport, renderModes) { const actors = viewport.getActors?.() ?? []; const sourceDataId = viewport.getSourceDataId?.(); const renderModeEntries = Object.entries(renderModes); if (renderModeEntries.length) { return renderModeEntries.map(([dataId, renderMode]) => { const role = resolveBindingRole(viewport, dataId, sourceDataId); const actor = role === 'source' ? viewport.getDefaultActor?.() : findActorForDataId(actors, dataId); return { actorUID: actor?.uid, dataId, referencedId: actor?.referencedId, renderMode, role, ...getActorScalarInfo(actor), }; }); } return actors.map((actor) => { const dataId = actor.representationUID ?? actor.uid ?? ''; return { actorUID: actor.uid, dataId, referencedId: actor.referencedId, renderMode: '-', role: resolveBindingRole(viewport, dataId, sourceDataId), ...getActorScalarInfo(actor), }; }); } updateMemoryPanel() { const memPanel = this.panels.get(PanelType.MEMORY); if (!memPanel) { return; } const perf = performance; if (perf.memory) { const memoryMB = perf.memory.usedJSHeapSize / CONVERSION.BYTES_TO_MB; const maxMemoryMB = perf.memory.jsHeapSizeLimit / CONVERSION.BYTES_TO_MB; memPanel.update(memoryMB, maxMemoryMB); } } attachDragHandlers() { this.dom?.addEventListener('pointerdown', this.handlePointerDown); } detachDragHandlers() { this.dom?.removeEventListener('pointerdown', this.handlePointerDown); window.removeEventListener('pointermove', this.handlePointerMove); window.removeEventListener('pointerup', this.handlePointerUp); window.removeEventListener('pointercancel', this.handlePointerUp); } onPointerDown(event) { if (!this.dom || event.button !== 0) { return; } this.dragPointerId = event.pointerId; const rect = this.dom.getBoundingClientRect(); this.dragOffsetX = event.clientX - rect.left; this.dragOffsetY = event.clientY - rect.top; this.setPosition(rect.left, rect.top); window.addEventListener('pointermove', this.handlePointerMove); window.addEventListener('pointerup', this.handlePointerUp); window.addEventListener('pointercancel', this.handlePointerUp); event.preventDefault(); } onPointerMove(event) { if (this.dragPointerId !== event.pointerId || !this.dom) { return; } this.setPosition(event.clientX - this.dragOffsetX, event.clientY - this.dragOffsetY); } onPointerUp(event) { if (this.dragPointerId !== event.pointerId || !this.dom) { return; } this.dragPointerId = null; window.removeEventListener('pointermove', this.handlePointerMove); window.removeEventListener('pointerup', this.handlePointerUp); window.removeEventListener('pointercancel', this.handlePointerUp); const rect = this.dom.getBoundingClientRect(); this.savePosition(rect.left, rect.top); } setPosition(left, top) { if (!this.dom) { return; } const clamped = this.clampToViewport(left, top); this.dom.style.left = `${clamped.left}px`; this.dom.style.top = `${clamped.top}px`; this.dom.style.right = 'auto'; this.dom.style.bottom = 'auto'; } clampToViewport(left, top) { if (!this.dom) { return { left, top }; } const width = this.dom.offsetWidth; const height = this.dom.offsetHeight; const maxLeft = Math.max(0, window.innerWidth - width); const maxTop = Math.max(0, window.innerHeight - height); return { left: Math.min(Math.max(0, left), maxLeft), top: Math.min(Math.max(0, top), maxTop), }; } restorePosition() { const saved = this.readSavedPosition(); if (!saved) { return; } this.setPosition(saved.left, saved.top); } readSavedPosition() { try { const raw = window.localStorage.getItem(STATS_CONFIG.POSITION_STORAGE_KEY); if (!raw) { return null; } const parsed = JSON.parse(raw); if (typeof parsed?.left !== 'number' || typeof parsed?.top !== 'number' || !Number.isFinite(parsed.left) || !Number.isFinite(parsed.top)) { return null; } return { left: parsed.left, top: parsed.top }; } catch { return null; } } savePosition(left, top) { try { window.localStorage.setItem(STATS_CONFIG.POSITION_STORAGE_KEY, JSON.stringify({ left, top })); } catch { } } } StatsOverlay.instance = null; function resolveBindingRole(viewport, dataId, sourceDataId) { const role = viewport.getDisplaySetRole?.(dataId); if (role === 'source' || role === 'overlay') { return role; } if (sourceDataId && dataId === sourceDataId) { return 'source'; } return 'data'; } function findActorForDataId(actors, dataId) { return actors.find((actor) => actor.uid === dataId || actor.representationUID === dataId || actor.referencedId === dataId); } function getActorScalarInfo(entry) { const inputData = entry?.actor?.getMapper?.()?.getInputData?.(); if (!inputData) { return {}; } try { const scalars = inputData.getPointData?.()?.getScalars?.(); if (scalars?.getDataType) { const numberOfComponents = scalars.getNumberOfComponents?.() ?? 1; const range = numberOfComponents > 1 ? scalars.getRange?.(0) : scalars.getRange?.(); return { scalarType: scalars.getDataType(), scalarRange: normalizeRange(range), numberOfComponents, }; } const scalarData = inputData.getScalarData?.(); if (scalarData?.constructor?.name) { const range = Number.isFinite(inputData.minPixelValue) && Number.isFinite(inputData.maxPixelValue) ? [inputData.minPixelValue, inputData.maxPixelValue] : undefined; return { scalarType: scalarData.constructor.name, scalarRange: normalizeRange(range), }; } } catch { } return {}; } function normalizeRange(range) { if (!range || range.length < 2 || !Number.isFinite(range[0]) || !Number.isFinite(range[1])) { return undefined; } return [range[0], range[1]]; }