Skip to content

Commit

Permalink
feat: actually resize canvas during preview. Also give message to use…
Browse files Browse the repository at this point in the history
…r if trying to close during screenshot.
  • Loading branch information
seankmartin committed Nov 20, 2024
1 parent dc50a09 commit 3f3431c
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 79 deletions.
7 changes: 5 additions & 2 deletions src/display_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/perspective_view/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ export class PerspectivePanel extends RenderedDataPanel {
}

ensureBoundsUpdated() {
super.ensureBoundsUpdated();
super.ensureBoundsUpdated(true /* canScaleForScreenshot */);
this.projectionParameters.setViewport(this.renderViewport);
}

Expand Down
2 changes: 1 addition & 1 deletion src/sliceview/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export class SliceViewPanel extends RenderedDataPanel {
}

ensureBoundsUpdated() {
super.ensureBoundsUpdated();
super.ensureBoundsUpdated(true /* canScaleForScreenshot */);
this.sliceView.projectionParameters.setViewport(this.renderViewport);
}

Expand Down
61 changes: 26 additions & 35 deletions src/ui/screenshot_menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -252,7 +268,7 @@ export class ScreenshotDialog extends Overlay {

this.closeMenuButton = this.createButton(
null,
() => this.dispose(),
() => this.close(),
"neuroglancer-screenshot-close-button",
svg_close,
);
Expand Down Expand Up @@ -366,6 +382,7 @@ export class ScreenshotDialog extends Overlay {
);
this.content.appendChild(this.footerScreenshotActionBtnsContainer);

this.screenshotManager.previewScreenshot();
this.updateUIBasedOnMode();
this.populatePanelResolutionTable();
this.throttledUpdateTableStatistics();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -645,7 +659,7 @@ export class ScreenshotDialog extends Overlay {
}

private cancelScreenshot() {
this.screenshotManager.cancelScreenshot();
this.screenshotManager.cancelScreenshot(true /* shouldStayInPrevieMenu */);
this.updateUIBasedOnMode();
}

Expand All @@ -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";
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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() {
Expand Down
94 changes: 64 additions & 30 deletions src/util/screenshot_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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;
Expand All @@ -237,67 +247,67 @@ 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;
} {
const renderingPanelArea = calculatePanelViewportBounds(
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();
}
}

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:
Expand All @@ -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);
}

/**
Expand Down
Loading

0 comments on commit 3f3431c

Please sign in to comment.