@cornerstonejs/core
Version:
Cornerstone3D Core
369 lines (368 loc) • 13.5 kB
JavaScript
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]];
}