Skip to content

Commit 3f36750

Browse files
committed
feat: add resetCameraScreenSpace method with offsetRatio input
add resetCameraScreenSpace method with offsetRatio input, essentially a resetCamera call that fills the entire screen space (where 90% offsetRatio means visible actors will fill 90% of the screen space) #1285
1 parent 8ecd009 commit 3f36750

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

Sources/Rendering/Core/Renderer/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,14 @@ export interface vtkRenderer extends vtkViewport {
611611
*/
612612
resetCamera(bounds?: Bounds): boolean;
613613

614+
/**
615+
* Reset the camera accounting for viewport aspect ratio to prevent cropping.
616+
* This is similar to resetCamera but computes the parallel scale from view space
617+
* dimensions, which fixes issues where narrow viewports crop significantly.
618+
* @param {Number} [offsetRatio=0.9] The fraction of space to use (default 0.9 = 90%, leaving 10% margin)
619+
*/
620+
resetCameraScreenSpace(offsetRatio?: number): boolean;
621+
614622
/**
615623
* Reset the camera clipping range based on a bounding box.
616624
* @param {Bounds} [bounds]

Sources/Rendering/Core/Renderer/index.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,149 @@ function vtkRenderer(publicAPI, model) {
464464
return true;
465465
};
466466

467+
publicAPI.resetCameraScreenSpace = (offsetRatio = 0.9) => {
468+
const boundsToUse = publicAPI.computeVisiblePropBounds();
469+
const center = [0, 0, 0];
470+
471+
if (!vtkMath.areBoundsInitialized(boundsToUse)) {
472+
vtkDebugMacro('Cannot reset camera!');
473+
return false;
474+
}
475+
476+
let vn = null;
477+
478+
if (publicAPI.getActiveCamera()) {
479+
vn = model.activeCamera.getViewPlaneNormal();
480+
} else {
481+
vtkErrorMacro('Trying to reset non-existent camera');
482+
return false;
483+
}
484+
485+
// Reset the perspective zoom factors, otherwise subsequent zooms will cause
486+
// the view angle to become very small and cause bad depth sorting.
487+
model.activeCamera.setViewAngle(30.0);
488+
489+
center[0] = (boundsToUse[0] + boundsToUse[1]) / 2.0;
490+
center[1] = (boundsToUse[2] + boundsToUse[3]) / 2.0;
491+
center[2] = (boundsToUse[4] + boundsToUse[5]) / 2.0;
492+
493+
let w1 = boundsToUse[1] - boundsToUse[0];
494+
let w2 = boundsToUse[3] - boundsToUse[2];
495+
let w3 = boundsToUse[5] - boundsToUse[4];
496+
w1 *= w1;
497+
w2 *= w2;
498+
w3 *= w3;
499+
let radius = w1 + w2 + w3;
500+
501+
// If we have just a single point, pick a radius of 1.0
502+
radius = radius === 0 ? 1.0 : radius;
503+
504+
// compute the radius of the enclosing sphere
505+
radius = Math.sqrt(radius) * 0.5;
506+
507+
const angle = vtkMath.radiansFromDegrees(model.activeCamera.getViewAngle());
508+
const distance = radius / Math.sin(angle * 0.5);
509+
510+
// check view-up vector against view plane normal
511+
const vup = model.activeCamera.getViewUp();
512+
if (Math.abs(vtkMath.dot(vup, vn)) > 0.999) {
513+
vtkWarningMacro('Resetting view-up since view plane normal is parallel');
514+
model.activeCamera.setViewUp(-vup[2], vup[0], vup[1]);
515+
}
516+
517+
// Set up camera position and focal point first (needed for view matrix)
518+
model.activeCamera.setFocalPoint(center[0], center[1], center[2]);
519+
model.activeCamera.setPosition(
520+
center[0] + distance * vn[0],
521+
center[1] + distance * vn[1],
522+
center[2] + distance * vn[2]
523+
);
524+
525+
// Calculate parallel scale accounting for viewport aspect ratio
526+
// This mirrors C++ VTK behavior by transforming bounds to view space
527+
// and computing the parallel scale from view space dimensions.
528+
// This fixes the issue where narrow viewports crop significantly (issue #1285)
529+
let parallelScale = radius;
530+
531+
// For parallel projection, compute parallel scale from view space bounds
532+
if (model._renderWindow && model.activeCamera.getParallelProjection()) {
533+
try {
534+
// Get the view from render window to access viewport size
535+
const views = model._renderWindow.getViews
536+
? model._renderWindow.getViews()
537+
: [];
538+
if (views.length > 0) {
539+
const view = views[0];
540+
const dims = view.getViewportSize
541+
? view.getViewportSize(publicAPI)
542+
: null;
543+
if (dims && dims[0] > 0 && dims[1] > 0) {
544+
const aspect = dims[0] / dims[1];
545+
546+
// Get corner points of the bounds in world space
547+
const visiblePoints = [];
548+
vtkBoundingBox.getCorners(boundsToUse, visiblePoints);
549+
550+
// Transform bounds to view space using the view matrix
551+
// The view matrix is now valid since we've set up the camera
552+
const viewBounds = vtkBoundingBox.reset([]);
553+
const viewMatrix = model.activeCamera.getViewMatrix();
554+
const viewMatrixTransposed = new Float64Array(16);
555+
mat4.copy(viewMatrixTransposed, viewMatrix);
556+
mat4.transpose(viewMatrixTransposed, viewMatrixTransposed);
557+
558+
for (let i = 0; i < visiblePoints.length; ++i) {
559+
const point = visiblePoints[i];
560+
const viewPoint = new Float64Array(3);
561+
vec3.transformMat4(viewPoint, point, viewMatrixTransposed);
562+
vtkBoundingBox.addPoint(viewBounds, ...viewPoint);
563+
}
564+
565+
// Get lengths in view space
566+
const xLength = vtkBoundingBox.getLength(viewBounds, 0);
567+
const yLength = vtkBoundingBox.getLength(viewBounds, 1);
568+
569+
// Apply offset ratio to add white space buffer
570+
// offsetRatio is the fraction of space to use (default 0.9 = 90%, leaving 10% margin)
571+
const marginMultiplier = 1.0 / offsetRatio;
572+
const xLengthWithMargin = marginMultiplier * xLength;
573+
const yLengthWithMargin = marginMultiplier * yLength;
574+
575+
// Use max of height and width/aspect to ensure everything fits
576+
// This accounts for viewport aspect ratio to prevent cropping
577+
// This mirrors C++ VTK behavior
578+
parallelScale =
579+
0.5 * Math.max(yLengthWithMargin, xLengthWithMargin / aspect);
580+
}
581+
}
582+
} catch (e) {
583+
// If we can't get aspect ratio, fall back to using radius
584+
vtkDebugMacro(
585+
'ResetCameraScreenSpace could not get aspect ratio, using radius for parallel scale'
586+
);
587+
}
588+
}
589+
590+
publicAPI.resetCameraClippingRange(boundsToUse);
591+
592+
// setup parallel scale (computed from view space for parallel projection)
593+
model.activeCamera.setParallelScale(parallelScale);
594+
595+
// update reasonable world to physical values
596+
model.activeCamera.setPhysicalScale(radius);
597+
model.activeCamera.setPhysicalTranslation(
598+
-center[0],
599+
-center[1],
600+
-center[2]
601+
);
602+
603+
// Here to let parallel/distributed compositing intercept
604+
// and do the right thing.
605+
publicAPI.invokeEvent(RESET_CAMERA_EVENT);
606+
607+
return true;
608+
};
609+
467610
publicAPI.resetCameraClippingRange = (bounds = null) => {
468611
const boundsToUse = bounds || publicAPI.computeVisiblePropBounds();
469612

0 commit comments

Comments
 (0)