Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/Proxy/Core/ViewProxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ function vtkViewProxy(publicAPI, model) {

// --------------------------------------------------------------------------

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

// --------------------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions Sources/Rendering/Core/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ function vtkRenderWindow(publicAPI, model) {
return results;
};

publicAPI.captureImages = (format = 'image/png') => {
publicAPI.captureImages = (format = 'image/png', opts = {}) => {
macro.setImmediate(publicAPI.render);
return model.views
.map((view) =>
view.captureNextImage ? view.captureNextImage(format) : undefined
view.captureNextImage ? view.captureNextImage(format, opts) : undefined
)
.filter((i) => !!i);
};
Expand Down
13 changes: 13 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,16 @@ Initialize the rendering window. This will setup all system-specific
resources. This method and Finalize() must be symmetric and it
should be possible to call them multiple times, even changing WindowId
in-between. This is what WindowRemap does.

### captureNextImage(format, options);

Capture a screenshot of the contents of this renderwindow. The options object
can include a `size` array (`[w, h]`) or a `scale` floating point value, as well
as a `resetCamera` boolean. If `size` is provided, the captured screenshot will
be of the given size (and `resetCamera` could be useful in this case if the
aspect ratio of `size` does not match the current renderwindow size). Otherwise,
if `scale` is provided, it will be multiplied by the current renderwindow size
to compute the screenshot size. If no `size` or `scale` are provided, the
current renderwindow size is assumed. The default format is "image/png".

Returns a promise that resolves to the captured screenshot.
98 changes: 94 additions & 4 deletions Sources/Rendering/OpenGL/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';

const { vtkDebugMacro, vtkErrorMacro } = macro;
const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
const SCREENSHOT_PLACEHOLDER = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};

function checkRenderTargetSupport(gl, format, type) {
// create temporary frame buffer and texture
Expand Down Expand Up @@ -522,19 +529,102 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
publicAPI.invokeImageReady(screenshot);
}

publicAPI.captureNextImage = (format = 'image/png') => {
publicAPI.captureNextImage = (
format = 'image/png',
{ resetCamera = false, size = null, scale = 1 } = {}
) => {
if (model.deleted) {
return null;
}
model.imageFormat = format;
const previous = model.notifyStartCaptureImage;
model.notifyStartCaptureImage = true;

model._screenshot = {
size:
!!size || scale !== 1
? size || model.size.map((val) => val * scale)
: null,
};

return new Promise((resolve, reject) => {
const subscription = publicAPI.onImageReady((imageURL) => {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
resolve(imageURL);
if (model._screenshot.size === null) {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
if (model._screenshot.placeHolder) {
// resize the main canvas back to its original size and show it
model.size = model._screenshot.originalSize;

// process the resize
publicAPI.modified();

// restore the saved camera parameters, if applicable
if (model._screenshot.cameras) {
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
restoreParamsFn(arg)
);
}

// Trigger a render at the original size
publicAPI.traverseAllPasses();

// Remove and clean up the placeholder, revealing the original
model.el.removeChild(model._screenshot.placeHolder);
model._screenshot.placeHolder.remove();
model._screenshot = null;
}
resolve(imageURL);
} else {
// Create a placeholder image overlay while we resize and render
const tmpImg = document.createElement('img');
tmpImg.style = SCREENSHOT_PLACEHOLDER;
tmpImg.src = imageURL;
model._screenshot.placeHolder = model.el.appendChild(tmpImg);

// hide the main canvas
model.canvas.style.display = 'none';

// remember the main canvas original size, then resize it
model._screenshot.originalSize = model.size;
model.size = model._screenshot.size;
model._screenshot.size = null;

// process the resize
publicAPI.modified();

if (resetCamera) {
// If resetCamera was requested, we first save camera parameters
// from all the renderers, so we can restore them later
model._screenshot.cameras = model.renderable
.getRenderers()
.map((renderer) => {
const camera = renderer.getActiveCamera();
const params = camera.get(
'focalPoint',
'position',
'parallelScale'
);

return {
resetCameraFn: renderer.resetCamera,
restoreParamsFn: camera.set,
// "clone" the params so we don't keep refs to properties
arg: JSON.parse(JSON.stringify(params)),
};
});

// Perform the resetCamera() on each renderer only after capturing
// the params from all active cameras, in case there happen to be
// linked cameras among the renderers.
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
resetCameraFn()
);
}

// Trigger a render at the custom size
publicAPI.traverseAllPasses();
}
});
});
};
Expand Down
98 changes: 94 additions & 4 deletions Sources/Rendering/WebGPU/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderW

const { vtkErrorMacro } = macro;
// const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
const SCREENSHOT_PLACEHOLDER = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};

// ----------------------------------------------------------------------------
// vtkWebGPURenderWindow methods
Expand Down Expand Up @@ -244,19 +251,102 @@ function vtkWebGPURenderWindow(publicAPI, model) {
publicAPI.invokeImageReady(screenshot);
}

publicAPI.captureNextImage = (format = 'image/png') => {
publicAPI.captureNextImage = (
format = 'image/png',
{ resetCamera = false, size = null, scale = 1 } = {}
) => {
if (model.deleted) {
return null;
}
model.imageFormat = format;
const previous = model.notifyStartCaptureImage;
model.notifyStartCaptureImage = true;

model._screenshot = {
size:
!!size || scale !== 1
? size || model.size.map((val) => val * scale)
: null,
};

return new Promise((resolve, reject) => {
const subscription = publicAPI.onImageReady((imageURL) => {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
resolve(imageURL);
if (model._screenshot.size === null) {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
if (model._screenshot.placeHolder) {
// resize the main canvas back to its original size and show it
model.size = model._screenshot.originalSize;

// process the resize
publicAPI.modified();

// restore the saved camera parameters, if applicable
if (model._screenshot.cameras) {
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
restoreParamsFn(arg)
);
}

// Trigger a render at the original size
publicAPI.traverseAllPasses();

// Remove and clean up the placeholder, revealing the original
model.el.removeChild(model._screenshot.placeHolder);
model._screenshot.placeHolder.remove();
model._screenshot = null;
}
resolve(imageURL);
} else {
// Create a placeholder image overlay while we resize and render
const tmpImg = document.createElement('img');
tmpImg.style = SCREENSHOT_PLACEHOLDER;
tmpImg.src = imageURL;
model._screenshot.placeHolder = model.el.appendChild(tmpImg);

// hide the main canvas
model.canvas.style.display = 'none';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see you reversing it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reversing it is done in the updateWindow method as a result of calling publicAPI.modified().


// remember the main canvas original size, then resize it
model._screenshot.originalSize = model.size;
model.size = model._screenshot.size;
model._screenshot.size = null;

// process the resize
publicAPI.modified();

if (resetCamera) {
// If resetCamera was requested, we first save camera parameters
// from all the renderers, so we can restore them later
model._screenshot.cameras = model.renderable
.getRenderers()
.map((renderer) => {
const camera = renderer.getActiveCamera();
const params = camera.get(
'focalPoint',
'position',
'parallelScale'
);

return {
resetCameraFn: renderer.resetCamera,
restoreParamsFn: camera.set,
// "clone" the params so we don't keep refs to properties
arg: JSON.parse(JSON.stringify(params)),
};
});

// Perform the resetCamera() on each renderer only after capturing
// the params from all active cameras, in case there happen to be
// linked cameras among the renderers.
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
resetCameraFn()
);
}

// Trigger a render at the custom size
publicAPI.traverseAllPasses();
}
});
});
};
Expand Down