Skip to content

Commit 12f97e6

Browse files
authored
Merge pull request #1907 from Kitware/resizable-screenshots
fix(RenderWindow): Support custom sizes for screenshots
2 parents 10e4e6b + ab45a23 commit 12f97e6

File tree

5 files changed

+205
-11
lines changed

5 files changed

+205
-11
lines changed

Sources/Proxy/Core/ViewProxy/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ function vtkViewProxy(publicAPI, model) {
263263

264264
// --------------------------------------------------------------------------
265265

266-
publicAPI.captureImage = () => model.renderWindow.captureImages()[0];
266+
publicAPI.captureImage = ({ format = 'image/png', ...opts } = {}) =>
267+
model.renderWindow.captureImages(format, opts)[0];
267268

268269
// --------------------------------------------------------------------------
269270

Sources/Rendering/Core/RenderWindow/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ function vtkRenderWindow(publicAPI, model) {
112112
return results;
113113
};
114114

115-
publicAPI.captureImages = (format = 'image/png') => {
115+
publicAPI.captureImages = (format = 'image/png', opts = {}) => {
116116
macro.setImmediate(publicAPI.render);
117117
return model.views
118118
.map((view) =>
119-
view.captureNextImage ? view.captureNextImage(format) : undefined
119+
view.captureNextImage ? view.captureNextImage(format, opts) : undefined
120120
)
121121
.filter((i) => !!i);
122122
};

Sources/Rendering/OpenGL/RenderWindow/api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,16 @@ Initialize the rendering window. This will setup all system-specific
3232
resources. This method and Finalize() must be symmetric and it
3333
should be possible to call them multiple times, even changing WindowId
3434
in-between. This is what WindowRemap does.
35+
36+
### captureNextImage(format, options);
37+
38+
Capture a screenshot of the contents of this renderwindow. The options object
39+
can include a `size` array (`[w, h]`) or a `scale` floating point value, as well
40+
as a `resetCamera` boolean. If `size` is provided, the captured screenshot will
41+
be of the given size (and `resetCamera` could be useful in this case if the
42+
aspect ratio of `size` does not match the current renderwindow size). Otherwise,
43+
if `scale` is provided, it will be multiplied by the current renderwindow size
44+
to compute the screenshot size. If no `size` or `scale` are provided, the
45+
current renderwindow size is assumed. The default format is "image/png".
46+
47+
Returns a promise that resolves to the captured screenshot.

Sources/Rendering/OpenGL/RenderWindow/index.js

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';
1111

1212
const { vtkDebugMacro, vtkErrorMacro } = macro;
1313
const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
14+
const SCREENSHOT_PLACEHOLDER = {
15+
position: 'absolute',
16+
top: 0,
17+
left: 0,
18+
width: '100%',
19+
height: '100%',
20+
};
1421

1522
function checkRenderTargetSupport(gl, format, type) {
1623
// create temporary frame buffer and texture
@@ -522,19 +529,102 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
522529
publicAPI.invokeImageReady(screenshot);
523530
}
524531

525-
publicAPI.captureNextImage = (format = 'image/png') => {
532+
publicAPI.captureNextImage = (
533+
format = 'image/png',
534+
{ resetCamera = false, size = null, scale = 1 } = {}
535+
) => {
526536
if (model.deleted) {
527537
return null;
528538
}
529539
model.imageFormat = format;
530540
const previous = model.notifyStartCaptureImage;
531541
model.notifyStartCaptureImage = true;
532542

543+
model._screenshot = {
544+
size:
545+
!!size || scale !== 1
546+
? size || model.size.map((val) => val * scale)
547+
: null,
548+
};
549+
533550
return new Promise((resolve, reject) => {
534551
const subscription = publicAPI.onImageReady((imageURL) => {
535-
model.notifyStartCaptureImage = previous;
536-
subscription.unsubscribe();
537-
resolve(imageURL);
552+
if (model._screenshot.size === null) {
553+
model.notifyStartCaptureImage = previous;
554+
subscription.unsubscribe();
555+
if (model._screenshot.placeHolder) {
556+
// resize the main canvas back to its original size and show it
557+
model.size = model._screenshot.originalSize;
558+
559+
// process the resize
560+
publicAPI.modified();
561+
562+
// restore the saved camera parameters, if applicable
563+
if (model._screenshot.cameras) {
564+
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
565+
restoreParamsFn(arg)
566+
);
567+
}
568+
569+
// Trigger a render at the original size
570+
publicAPI.traverseAllPasses();
571+
572+
// Remove and clean up the placeholder, revealing the original
573+
model.el.removeChild(model._screenshot.placeHolder);
574+
model._screenshot.placeHolder.remove();
575+
model._screenshot = null;
576+
}
577+
resolve(imageURL);
578+
} else {
579+
// Create a placeholder image overlay while we resize and render
580+
const tmpImg = document.createElement('img');
581+
tmpImg.style = SCREENSHOT_PLACEHOLDER;
582+
tmpImg.src = imageURL;
583+
model._screenshot.placeHolder = model.el.appendChild(tmpImg);
584+
585+
// hide the main canvas
586+
model.canvas.style.display = 'none';
587+
588+
// remember the main canvas original size, then resize it
589+
model._screenshot.originalSize = model.size;
590+
model.size = model._screenshot.size;
591+
model._screenshot.size = null;
592+
593+
// process the resize
594+
publicAPI.modified();
595+
596+
if (resetCamera) {
597+
// If resetCamera was requested, we first save camera parameters
598+
// from all the renderers, so we can restore them later
599+
model._screenshot.cameras = model.renderable
600+
.getRenderers()
601+
.map((renderer) => {
602+
const camera = renderer.getActiveCamera();
603+
const params = camera.get(
604+
'focalPoint',
605+
'position',
606+
'parallelScale'
607+
);
608+
609+
return {
610+
resetCameraFn: renderer.resetCamera,
611+
restoreParamsFn: camera.set,
612+
// "clone" the params so we don't keep refs to properties
613+
arg: JSON.parse(JSON.stringify(params)),
614+
};
615+
});
616+
617+
// Perform the resetCamera() on each renderer only after capturing
618+
// the params from all active cameras, in case there happen to be
619+
// linked cameras among the renderers.
620+
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
621+
resetCameraFn()
622+
);
623+
}
624+
625+
// Trigger a render at the custom size
626+
publicAPI.traverseAllPasses();
627+
}
538628
});
539629
});
540630
};

Sources/Rendering/WebGPU/RenderWindow/index.js

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderW
1212

1313
const { vtkErrorMacro } = macro;
1414
// const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
15+
const SCREENSHOT_PLACEHOLDER = {
16+
position: 'absolute',
17+
top: 0,
18+
left: 0,
19+
width: '100%',
20+
height: '100%',
21+
};
1522

1623
// ----------------------------------------------------------------------------
1724
// vtkWebGPURenderWindow methods
@@ -244,19 +251,102 @@ function vtkWebGPURenderWindow(publicAPI, model) {
244251
publicAPI.invokeImageReady(screenshot);
245252
}
246253

247-
publicAPI.captureNextImage = (format = 'image/png') => {
254+
publicAPI.captureNextImage = (
255+
format = 'image/png',
256+
{ resetCamera = false, size = null, scale = 1 } = {}
257+
) => {
248258
if (model.deleted) {
249259
return null;
250260
}
251261
model.imageFormat = format;
252262
const previous = model.notifyStartCaptureImage;
253263
model.notifyStartCaptureImage = true;
254264

265+
model._screenshot = {
266+
size:
267+
!!size || scale !== 1
268+
? size || model.size.map((val) => val * scale)
269+
: null,
270+
};
271+
255272
return new Promise((resolve, reject) => {
256273
const subscription = publicAPI.onImageReady((imageURL) => {
257-
model.notifyStartCaptureImage = previous;
258-
subscription.unsubscribe();
259-
resolve(imageURL);
274+
if (model._screenshot.size === null) {
275+
model.notifyStartCaptureImage = previous;
276+
subscription.unsubscribe();
277+
if (model._screenshot.placeHolder) {
278+
// resize the main canvas back to its original size and show it
279+
model.size = model._screenshot.originalSize;
280+
281+
// process the resize
282+
publicAPI.modified();
283+
284+
// restore the saved camera parameters, if applicable
285+
if (model._screenshot.cameras) {
286+
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
287+
restoreParamsFn(arg)
288+
);
289+
}
290+
291+
// Trigger a render at the original size
292+
publicAPI.traverseAllPasses();
293+
294+
// Remove and clean up the placeholder, revealing the original
295+
model.el.removeChild(model._screenshot.placeHolder);
296+
model._screenshot.placeHolder.remove();
297+
model._screenshot = null;
298+
}
299+
resolve(imageURL);
300+
} else {
301+
// Create a placeholder image overlay while we resize and render
302+
const tmpImg = document.createElement('img');
303+
tmpImg.style = SCREENSHOT_PLACEHOLDER;
304+
tmpImg.src = imageURL;
305+
model._screenshot.placeHolder = model.el.appendChild(tmpImg);
306+
307+
// hide the main canvas
308+
model.canvas.style.display = 'none';
309+
310+
// remember the main canvas original size, then resize it
311+
model._screenshot.originalSize = model.size;
312+
model.size = model._screenshot.size;
313+
model._screenshot.size = null;
314+
315+
// process the resize
316+
publicAPI.modified();
317+
318+
if (resetCamera) {
319+
// If resetCamera was requested, we first save camera parameters
320+
// from all the renderers, so we can restore them later
321+
model._screenshot.cameras = model.renderable
322+
.getRenderers()
323+
.map((renderer) => {
324+
const camera = renderer.getActiveCamera();
325+
const params = camera.get(
326+
'focalPoint',
327+
'position',
328+
'parallelScale'
329+
);
330+
331+
return {
332+
resetCameraFn: renderer.resetCamera,
333+
restoreParamsFn: camera.set,
334+
// "clone" the params so we don't keep refs to properties
335+
arg: JSON.parse(JSON.stringify(params)),
336+
};
337+
});
338+
339+
// Perform the resetCamera() on each renderer only after capturing
340+
// the params from all active cameras, in case there happen to be
341+
// linked cameras among the renderers.
342+
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
343+
resetCameraFn()
344+
);
345+
}
346+
347+
// Trigger a render at the custom size
348+
publicAPI.traverseAllPasses();
349+
}
260350
});
261351
});
262352
};

0 commit comments

Comments
 (0)