diff --git a/src/display_context.ts b/src/display_context.ts index 6dae8b21b..d06157af1 100644 --- a/src/display_context.ts +++ b/src/display_context.ts @@ -139,7 +139,7 @@ export abstract class RenderedPanel extends RefCounted { abstract isReady(): boolean; - ensureBoundsUpdated() { + ensureBoundsUpdated(canScaleForScreenshot: boolean = false) { const { context } = this; context.ensureBoundsUpdated(); const { boundsGeneration } = context; @@ -225,7 +225,10 @@ export abstract class RenderedPanel extends RefCounted { 0, clippedBottom - clippedTop, )); - if (this.context.screenshotMode.value !== ScreenshotMode.OFF) { + if ( + this.context.screenshotMode.value !== ScreenshotMode.OFF && + canScaleForScreenshot + ) { viewport.width = logicalWidth * screenToCanvasPixelScaleX; viewport.height = logicalHeight * screenToCanvasPixelScaleY; viewport.logicalWidth = logicalWidth * screenToCanvasPixelScaleX; diff --git a/src/overlay.ts b/src/overlay.ts index 3703e05e2..8ead82ac0 100644 --- a/src/overlay.ts +++ b/src/overlay.ts @@ -47,11 +47,15 @@ export class Overlay extends RefCounted { document.body.appendChild(container); this.registerDisposer(new KeyboardEventBinder(this.container, this.keyMap)); this.registerEventListener(container, "action:close", () => { - this.dispose(); + this.close(); }); content.focus(); } + close() { + this.dispose(); + } + disposed() { --overlaysOpen; document.body.removeChild(this.container); diff --git a/src/perspective_view/panel.ts b/src/perspective_view/panel.ts index 2f0bceb0f..e51238670 100644 --- a/src/perspective_view/panel.ts +++ b/src/perspective_view/panel.ts @@ -605,7 +605,7 @@ export class PerspectivePanel extends RenderedDataPanel { } ensureBoundsUpdated() { - super.ensureBoundsUpdated(); + super.ensureBoundsUpdated(true /* canScaleForScreenshot */); this.projectionParameters.setViewport(this.renderViewport); } diff --git a/src/sliceview/panel.ts b/src/sliceview/panel.ts index 172ee6f25..a4d1ffd91 100644 --- a/src/sliceview/panel.ts +++ b/src/sliceview/panel.ts @@ -435,7 +435,7 @@ export class SliceViewPanel extends RenderedDataPanel { } ensureBoundsUpdated() { - super.ensureBoundsUpdated(); + super.ensureBoundsUpdated(true /* canScaleForScreenshot */); this.sliceView.projectionParameters.setViewport(this.renderViewport); } diff --git a/src/ui/screenshot_menu.ts b/src/ui/screenshot_menu.ts index fe2f28a26..aabb1821a 100644 --- a/src/ui/screenshot_menu.ts +++ b/src/ui/screenshot_menu.ts @@ -44,6 +44,7 @@ import { makeIcon } from "#src/widget/icon.js"; // Usually the user is locked into the screenshot menu until the screenshot is taken or cancelled // Setting this to true, and setting the SCREENSHOT_MENU_CLOSE_TIMEOUT in screenshot_manager.ts // to a high value can be useful for debugging canvas handling of the resize +// Also helpful for viewing the canvas at higher resolutions const DEBUG_ALLOW_MENU_CLOSE = false; // For easy access to UI elements @@ -121,9 +122,9 @@ function formatPhysicalResolution(resolution: DimensionResolutionStats[]) { }; } -function formatPixelResolution(panelArea: PanelViewport, scale: number) { - const width = Math.round(panelArea.right - panelArea.left) * scale; - const height = Math.round(panelArea.bottom - panelArea.top) * scale; +function formatPixelResolution(panelArea: PanelViewport) { + const width = Math.round(panelArea.right - panelArea.left); + const height = Math.round(panelArea.bottom - panelArea.top); const type = panelArea.panelType; return { width, height, type }; } @@ -199,6 +200,21 @@ export class ScreenshotDialog extends Overlay { if (!DEBUG_ALLOW_MENU_CLOSE) { this.screenshotManager.shouldKeepSliceViewFOVFixed = true; this.screenshotManager.screenshotScale = 1; + this.screenshotManager.cancelScreenshot(); + } + } + + close(): void { + if ( + this.screenshotMode !== ScreenshotMode.PREVIEW && + !DEBUG_ALLOW_MENU_CLOSE + ) { + StatusMessage.showTemporaryMessage( + "Cannot close screenshot menu while a screenshot is in progress. Hit 'Cancel screenshot' to stop the screenshot, or 'Force screenshot' to screenshot the currently available data.", + 4000, + ); + } else { + super.close(); } } @@ -252,7 +268,7 @@ export class ScreenshotDialog extends Overlay { this.closeMenuButton = this.createButton( null, - () => this.dispose(), + () => this.close(), "neuroglancer-screenshot-close-button", svg_close, ); @@ -366,6 +382,7 @@ export class ScreenshotDialog extends Overlay { ); this.content.appendChild(this.footerScreenshotActionBtnsContainer); + this.screenshotManager.previewScreenshot(); this.updateUIBasedOnMode(); this.populatePanelResolutionTable(); this.throttledUpdateTableStatistics(); @@ -579,10 +596,7 @@ export class ScreenshotDialog extends Overlay { const physicalResolution = formatPhysicalResolution( resolution.physicalResolution, ); - const pixelResolution = formatPixelResolution( - resolution.pixelResolution, - this.getResolutionScaleMultiplier(), - ); + const pixelResolution = formatPixelResolution(resolution.pixelResolution); const row = resolutionTable.insertRow(); const keyCell = row.insertCell(); const pixelValueCell = row.insertCell(); @@ -645,7 +659,7 @@ export class ScreenshotDialog extends Overlay { } private cancelScreenshot() { - this.screenshotManager.cancelScreenshot(); + this.screenshotManager.cancelScreenshot(true /* shouldStayInPrevieMenu */); this.updateUIBasedOnMode(); } @@ -672,9 +686,7 @@ export class ScreenshotDialog extends Overlay { private handleScreenshotResize() { const screenshotSize = - this.screenshotManager.calculatedScaledAndClippedSize( - this.getResolutionScaleMultiplier(), - ); + this.screenshotManager.calculatedClippedViewportSize(); if (screenshotSize.width * screenshotSize.height > LARGE_SCREENSHOT_SIZE) { this.warningElement.textContent = "Warning: large screenshots (bigger than 4096x4096) may fail"; @@ -736,10 +748,7 @@ export class ScreenshotDialog extends Overlay { // Process the panel resolution table const { panelResolutionData, layerResolutionData } = - getViewerResolutionMetadata( - this.screenshotManager.viewer, - this.getResolutionScaleMultiplier(), - ); + getViewerResolutionMetadata(this.screenshotManager.viewer); let panelResolutionText = `${PANEL_TABLE_HEADER_STRINGS.type}\t${PANEL_TABLE_HEADER_STRINGS.pixelResolution}\t${PANEL_TABLE_HEADER_STRINGS.physicalResolution}\n`; for (const resolution of panelResolutionData) { @@ -755,21 +764,8 @@ export class ScreenshotDialog extends Overlay { return `${screenshotSizeText}\n${panelResolutionText}\n${layerResolutionText}`; } - /** - * While the screenshot is not in progress, the user can change the scale of the screenshot - * We want to give a preview of the screenshot size to the user - * During the screenshot, the user is locked into the menu and cannot change the scale - * And the viewer canvas pixels have been resized to the screenshot size - * So the preview is not necessary - */ - private getResolutionScaleMultiplier() { - return this.screenshotMode === ScreenshotMode.OFF - ? this.screenshotManager.screenshotScale - : 1; - } - private updateUIBasedOnMode() { - if (this.screenshotMode === ScreenshotMode.OFF) { + if (this.screenshotMode === ScreenshotMode.PREVIEW) { this.nameInput.disabled = false; for (const radio of this.scaleRadioButtonsContainer.children) { for (const child of (radio as HTMLElement).children) { @@ -781,7 +777,6 @@ export class ScreenshotDialog extends Overlay { this.cancelScreenshotButton.disabled = true; this.takeScreenshotButton.disabled = false; this.progressText.textContent = ""; - this.closeMenuButton.disabled = false; this.forceScreenshotButton.title = ""; } else { this.nameInput.disabled = true; @@ -795,13 +790,9 @@ export class ScreenshotDialog extends Overlay { this.cancelScreenshotButton.disabled = false; this.takeScreenshotButton.disabled = true; this.progressText.textContent = "Screenshot in progress..."; - this.closeMenuButton.disabled = true; this.forceScreenshotButton.title = "Force a screenshot of the current view without waiting for all data to be loaded and rendered"; } - if (DEBUG_ALLOW_MENU_CLOSE) { - this.closeMenuButton.disabled = false; - } } get screenshotMode() { diff --git a/src/util/screenshot_manager.ts b/src/util/screenshot_manager.ts index ce9e59df2..8b08d462c 100644 --- a/src/util/screenshot_manager.ts +++ b/src/util/screenshot_manager.ts @@ -39,7 +39,7 @@ import { } from "#src/util/viewer_resolution_stats.js"; import type { Viewer } from "#src/viewer.js"; -const SCREENSHOT_TIMEOUT = 5000; +const SCREENSHOT_TIMEOUT = 3000; export interface ScreenshotLoadStatistics extends ScreenshotChunkStatistics { timestamp: number; @@ -207,7 +207,7 @@ export class ScreenshotManager extends RefCounted { } public set screenshotScale(scale: number) { - this.handleScreenshotZoom(scale); + this.handleScreenshotZoomAndResize(scale); this._screenshotScale = scale; this.zoomMaybeChanged.dispatch(); } @@ -220,14 +220,24 @@ export class ScreenshotManager extends RefCounted { const wasInFixedFOVMode = this.shouldKeepSliceViewFOVFixed; this._shouldKeepSliceViewFOVFixed = enableFixedFOV; if (!enableFixedFOV && wasInFixedFOVMode) { - this.handleScreenshotZoom(this.screenshotScale, true /* resetZoom */); + this.handleScreenshotZoomAndResize( + this.screenshotScale, + true /* resetZoom */, + ); this.zoomMaybeChanged.dispatch(); } else if (enableFixedFOV && !wasInFixedFOVMode) { - this.handleScreenshotZoom(1 / this.screenshotScale, true /* resetZoom */); + this.handleScreenshotZoomAndResize( + 1 / this.screenshotScale, + true /* resetZoom */, + ); this.zoomMaybeChanged.dispatch(); } } + previewScreenshot() { + this.viewer.display.screenshotMode.value = ScreenshotMode.PREVIEW; + } + takeScreenshot(filename: string = "") { this.filename = filename; this.viewer.display.screenshotMode.value = ScreenshotMode.ON; @@ -237,16 +247,19 @@ export class ScreenshotManager extends RefCounted { this.viewer.display.screenshotMode.value = ScreenshotMode.FORCE; } - cancelScreenshot() { + cancelScreenshot(shouldStayInPreview: boolean = false) { // Decrement the screenshot ID since the screenshot was cancelled if (this.screenshotMode === ScreenshotMode.ON) { this.screenshotId--; } - this.viewer.display.screenshotMode.value = ScreenshotMode.OFF; + const newMode = shouldStayInPreview + ? ScreenshotMode.PREVIEW + : ScreenshotMode.OFF; + this.viewer.display.screenshotMode.value = newMode; } - // Scales the screenshot by the given factor, and calculates the cropped area - calculatedScaledAndClippedSize(scale: number): { + // Calculates the cropped area of the viewport panels + calculatedClippedViewportSize(): { width: number; height: number; } { @@ -254,43 +267,38 @@ export class ScreenshotManager extends RefCounted { this.viewer.display.panels, ).totalRenderPanelViewport; return { - width: - Math.round(renderingPanelArea.right - renderingPanelArea.left) * scale, - height: - Math.round(renderingPanelArea.bottom - renderingPanelArea.top) * scale, + width: Math.round(renderingPanelArea.right - renderingPanelArea.left), + height: Math.round(renderingPanelArea.bottom - renderingPanelArea.top), }; } private handleScreenshotStarted() { - const { viewer } = this; - const shouldIncreaseCanvasSize = this.screenshotScale !== 1; - this.screenshotStartTime = this.lastUpdateTimestamp = this.gpuMemoryChangeTimestamp = Date.now(); this.screenshotLoadStats = null; - if (shouldIncreaseCanvasSize) { + // Pass a new screenshot ID to the viewer to trigger a new screenshot. + this.screenshotId++; + this.viewer.screenshotHandler.requestState.value = + this.screenshotId.toString(); + } + + private resizeCanvasIfNeeded(scale: number = this.screenshotScale) { + const shouldChangeCanvasSize = scale !== 1; + const { viewer } = this; + if (shouldChangeCanvasSize) { const oldSize = { width: viewer.display.canvas.width, height: viewer.display.canvas.height, }; const newSize = { - width: Math.round(oldSize.width * this.screenshotScale), - height: Math.round(oldSize.height * this.screenshotScale), + width: Math.round(oldSize.width * scale), + height: Math.round(oldSize.height * scale), }; viewer.display.canvas.width = newSize.width; viewer.display.canvas.height = newSize.height; - } - - // Pass a new screenshot ID to the viewer to trigger a new screenshot. - this.screenshotId++; - this.viewer.screenshotHandler.requestState.value = - this.screenshotId.toString(); - - // Force handling the canvas size change - if (shouldIncreaseCanvasSize) { ++viewer.display.resizeGeneration; viewer.display.resizeCallback(); } @@ -298,6 +306,8 @@ export class ScreenshotManager extends RefCounted { private handleScreenshotModeChange() { const { display } = this.viewer; + // If moving straight from OFF to ON, need to resize the canvas to the correct size + const mayNeedCanvasResize = this.screenshotMode === ScreenshotMode.OFF; this.screenshotMode = display.screenshotMode.value; switch (this.screenshotMode) { case ScreenshotMode.OFF: @@ -309,25 +319,49 @@ export class ScreenshotManager extends RefCounted { display.scheduleRedraw(); break; case ScreenshotMode.ON: + // If moving straight from OFF to ON, may need to resize the canvas to the correct size + // Going from PREVIEW to ON does not require a resize + if (mayNeedCanvasResize) { + this.resizeCanvasIfNeeded(); + } this.handleScreenshotStarted(); break; + case ScreenshotMode.PREVIEW: + // Do nothing, included for completeness + break; } } - private handleScreenshotZoom(scale: number, resetZoom: boolean = false) { + /** + * Handles the zooming of the screenshot in fixed FOV mode. + * This supports: + * 1. Updating the zoom level of the viewer to match the screenshot scale. + * 2. Resetting the zoom level of the slice views to the original level. + * 3. Resizing the canvas to match the new scale. + * @param scale - The scale factor to apply to the screenshot. + * @param resetZoom - If true, the zoom resets to the original level. + */ + private handleScreenshotZoomAndResize( + scale: number, + resetZoom: boolean = false, + ) { const oldScale = this.screenshotScale; - const scaleFactor = resetZoom ? scale : oldScale / scale; + const zoomScaleFactor = resetZoom ? scale : oldScale / scale; + const canvasScaleFactor = resetZoom ? 1 : scale / oldScale; if (this.shouldKeepSliceViewFOVFixed || resetZoom) { + // Scale the zoom factor of each slice view panel const { navigationState } = this.viewer; for (const panel of this.viewer.display.panels) { if (panel instanceof SliceViewPanel) { const zoom = navigationState.zoomFactor.value; - navigationState.zoomFactor.value = zoom * scaleFactor; + navigationState.zoomFactor.value = zoom * zoomScaleFactor; break; } } } + + this.resizeCanvasIfNeeded(canvasScaleFactor); } /** diff --git a/src/util/trackable_screenshot_mode.ts b/src/util/trackable_screenshot_mode.ts index d82f3d819..d6414a45d 100644 --- a/src/util/trackable_screenshot_mode.ts +++ b/src/util/trackable_screenshot_mode.ts @@ -18,8 +18,9 @@ import { TrackableEnum } from "#src/util/trackable_enum.js"; export enum ScreenshotMode { OFF = 0, // Default mode - ON = 1, // Screenshot modek + ON = 1, // Screenshot mode FORCE = 2, // Force screenshot mode - used when the screenshot is stuck + PREVIEW = 3, // Preview mode - used while the user is in the screenshot menu } export class TrackableScreenshotMode extends TrackableEnum { diff --git a/src/util/viewer_resolution_stats.ts b/src/util/viewer_resolution_stats.ts index d82a6c8b4..10951d22e 100644 --- a/src/util/viewer_resolution_stats.ts +++ b/src/util/viewer_resolution_stats.ts @@ -419,9 +419,9 @@ function formatPhysicalResolution(resolution: DimensionResolutionStats[]) { } } -function formatPixelResolution(panelArea: PanelViewport, scale: number) { - const width = Math.round(panelArea.right - panelArea.left) * scale; - const height = Math.round(panelArea.bottom - panelArea.top) * scale; +function formatPixelResolution(panelArea: PanelViewport) { + const width = Math.round(panelArea.right - panelArea.left); + const height = Math.round(panelArea.bottom - panelArea.top); const type = panelArea.panelType; return { width, height, type }; } @@ -437,7 +437,6 @@ function formatPixelResolution(panelArea: PanelViewport, scale: number) { */ export function getViewerResolutionMetadata( viewer: Viewer, - sliceViewScaleFactor: number = 1, ): ResolutionMetadata { // Process the panel resolution table const panelResolution = getViewerPanelResolutions(viewer.display.panels); @@ -449,10 +448,7 @@ export function getViewerResolutionMetadata( if (physicalResolution === null) { continue; } - const pixelResolution = formatPixelResolution( - resolution.pixelResolution, - sliceViewScaleFactor, - ); + const pixelResolution = formatPixelResolution(resolution.pixelResolution); panelResolutionData.push({ type: physicalResolution.type, width: pixelResolution.width,