Skip to content

Commit ef3e331

Browse files
committed
fix: resetCameraScreenSpace fully consistent with VTK cpp
#1285
1 parent 3f36750 commit ef3e331

File tree

2 files changed

+101
-121
lines changed

2 files changed

+101
-121
lines changed

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -612,10 +612,26 @@ export interface vtkRenderer extends vtkViewport {
612612
resetCamera(bounds?: Bounds): boolean;
613613

614614
/**
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)
615+
* Automatically set up the camera based on the visible actors, using a
616+
* screen-space bounding box to zoom closer to the data.
617+
*
618+
* This method first calls resetCamera to ensure all bounds are visible, then
619+
* projects the bounding box corners to screen space and zooms so the actors
620+
* fill the specified fraction of the viewport. This correctly accounts for
621+
* viewport aspect ratio for both perspective and parallel projection.
622+
*
623+
* Matches the VTK C++ vtkRenderer::ResetCameraScreenSpace behavior.
624+
*
625+
* @param {Bounds} [bounds] Optional bounding box to use. If not provided,
626+
* the visible prop bounds are computed automatically.
627+
* @param {Number} [offsetRatio=0.9] Fraction of screen space to fill
628+
* (0.9 = 90%, leaving 10% margin at the edges).
629+
*/
630+
resetCameraScreenSpace(bounds?: Bounds | null, offsetRatio?: number): boolean;
631+
632+
/**
633+
* Overload that accepts only offsetRatio (bounds are computed automatically).
634+
* @param {Number} offsetRatio Fraction of screen space to fill.
619635
*/
620636
resetCameraScreenSpace(offsetRatio?: number): boolean;
621637

Sources/Rendering/Core/Renderer/index.js

Lines changed: 81 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -464,144 +464,108 @@ 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];
467+
// Port of VTK C++ vtkRenderer::ResetCameraScreenSpace.
468+
// Uses a screen-space bounding box to zoom closer to the data.
469+
publicAPI.resetCameraScreenSpace = (bounds = null, offsetRatio = 0.9) => {
470+
let effectiveBounds = bounds;
471+
let effectiveOffsetRatio = offsetRatio;
472+
if (typeof bounds === 'number') {
473+
effectiveOffsetRatio = bounds;
474+
effectiveBounds = null;
475+
}
476+
477+
const boundsToUse = effectiveBounds || publicAPI.computeVisiblePropBounds();
470478

471479
if (!vtkMath.areBoundsInitialized(boundsToUse)) {
472480
vtkDebugMacro('Cannot reset camera!');
473481
return false;
474482
}
475483

476-
let vn = null;
484+
// Make sure all bounds are visible to project into screen space
485+
publicAPI.resetCamera(boundsToUse);
477486

478-
if (publicAPI.getActiveCamera()) {
479-
vn = model.activeCamera.getViewPlaneNormal();
480-
} else {
481-
vtkErrorMacro('Trying to reset non-existent camera');
482-
return false;
487+
// Get the view from the render window to access viewport size and
488+
// world-to-display conversion
489+
let view = null;
490+
if (model._renderWindow && model._renderWindow.getViews) {
491+
const views = model._renderWindow.getViews();
492+
if (views.length > 0) {
493+
view = views[0];
494+
}
483495
}
484496

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;
497+
if (!view || !view.getViewportSize) {
498+
return true;
499+
}
492500

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;
501+
const size = view.getViewportSize(publicAPI);
502+
if (!size || size[0] <= 0 || size[1] <= 0) {
503+
return true;
504+
}
500505

501-
// If we have just a single point, pick a radius of 1.0
502-
radius = radius === 0 ? 1.0 : radius;
506+
const aspect = size[0] / size[1];
503507

504-
// compute the radius of the enclosing sphere
505-
radius = Math.sqrt(radius) * 0.5;
508+
// Compute the screen-space bounding box by projecting all 8 corners
509+
let xmin = Number.MAX_VALUE;
510+
let ymin = Number.MAX_VALUE;
511+
let xmax = -Number.MAX_VALUE;
512+
let ymax = -Number.MAX_VALUE;
506513

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]);
514+
for (let i = 0; i < 2; ++i) {
515+
for (let j = 0; j < 2; ++j) {
516+
for (let k = 0; k < 2; ++k) {
517+
const nd = publicAPI.worldToNormalizedDisplay(
518+
boundsToUse[i],
519+
boundsToUse[2 + j],
520+
boundsToUse[4 + k],
521+
aspect
522+
);
523+
const dx = nd[0] * size[0];
524+
const dy = nd[1] * size[1];
525+
xmin = Math.min(dx, xmin);
526+
xmax = Math.max(dx, xmax);
527+
ymin = Math.min(dy, ymin);
528+
ymax = Math.max(dy, ymax);
529+
}
530+
}
515531
}
516532

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]
533+
// Project the focal point in screen space
534+
const fp = model.activeCamera.getFocalPoint();
535+
const fpNd = publicAPI.worldToNormalizedDisplay(
536+
fp[0],
537+
fp[1],
538+
fp[2],
539+
aspect
523540
);
541+
const fpDisplayX = fpNd[0] * size[0];
542+
const fpDisplayY = fpNd[1] * size[1];
524543

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-
}
544+
// The focal point must be at the center of the box
545+
const xCenterFocalPoint = Math.trunc(fpDisplayX);
546+
const yCenterFocalPoint = Math.trunc(fpDisplayY);
547+
const xCenterBox = Math.trunc((xmin + xmax) / 2);
548+
const yCenterBox = Math.trunc((ymin + ymax) / 2);
589549

590-
publicAPI.resetCameraClippingRange(boundsToUse);
550+
const xDiff = 2 * (xCenterFocalPoint - xCenterBox);
551+
const yDiff = 2 * (yCenterFocalPoint - yCenterBox);
591552

592-
// setup parallel scale (computed from view space for parallel projection)
593-
model.activeCamera.setParallelScale(parallelScale);
553+
xmin += Math.min(xDiff, 0);
554+
xmax += Math.max(xDiff, 0);
555+
ymin += Math.min(yDiff, 0);
556+
ymax += Math.max(yDiff, 0);
594557

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-
);
558+
// ZoomToBoxUsingViewAngle
559+
const boxWidth = xmax - xmin;
560+
const boxHeight = ymax - ymin;
561+
562+
if (boxWidth > 0 && boxHeight > 0) {
563+
const zf1 = size[0] / boxWidth;
564+
const zf2 = size[1] / boxHeight;
565+
const zoomFactor = Math.min(zf1, zf2);
566+
model.activeCamera.zoom(zoomFactor * effectiveOffsetRatio);
567+
}
602568

603-
// Here to let parallel/distributed compositing intercept
604-
// and do the right thing.
605569
publicAPI.invokeEvent(RESET_CAMERA_EVENT);
606570

607571
return true;

0 commit comments

Comments
 (0)