Skip to content

Commit cad750c

Browse files
committed
feat: add resetCameraScreenSpace method with offsetRatio input
Port of VTK C++ vtkRenderer::ResetCameraScreenSpace. Uses a screen-space bounding box to zoom closer to the data, correctly accounting for viewport aspect ratio for both perspective and parallel projection. Also adds zoomToBoxUsingViewAngle as a standalone public method. Closes #1285
1 parent 2d4d284 commit cad750c

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

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

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

614+
/**
615+
* Zoom the camera so the given screen-space box fills the viewport.
616+
*
617+
* Matches the VTK C++ vtkRenderer::ZoomToBoxUsingViewAngle behavior.
618+
*
619+
* @param {Object} box Screen-space rectangle with x, y, width, height
620+
* in display (pixel) coordinates.
621+
* @param {Number} [offsetRatio=1.0] Scale factor applied to the zoom
622+
* (values < 1 leave a margin around the box).
623+
*/
624+
zoomToBoxUsingViewAngle(
625+
box: { x: number; y: number; width: number; height: number },
626+
offsetRatio?: number
627+
): void;
628+
629+
/**
630+
* Automatically set up the camera based on the visible actors, using a
631+
* screen-space bounding box to zoom closer to the data.
632+
*
633+
* This method first calls resetCamera to ensure all bounds are visible, then
634+
* projects the bounding box corners to screen space and zooms so the actors
635+
* fill the specified fraction of the viewport. This correctly accounts for
636+
* viewport aspect ratio for both perspective and parallel projection.
637+
*
638+
* Matches the VTK C++ vtkRenderer::ResetCameraScreenSpace behavior.
639+
*
640+
* @param {Bounds} [bounds] Optional bounding box to use. If not provided,
641+
* the visible prop bounds are computed automatically.
642+
* @param {Number} [offsetRatio=0.9] Fraction of screen space to fill
643+
* (0.9 = 90%, leaving 10% margin at the edges).
644+
*/
645+
resetCameraScreenSpace(bounds?: Bounds | null, offsetRatio?: number): boolean;
646+
647+
/**
648+
* Overload that accepts only offsetRatio (bounds are computed automatically).
649+
* @param {Number} offsetRatio Fraction of screen space to fill.
650+
*/
651+
resetCameraScreenSpace(offsetRatio?: number): boolean;
652+
614653
/**
615654
* Reset the camera clipping range based on a bounding box.
616655
* @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+
// Port of VTK C++ vtkRenderer::ZoomToBoxUsingViewAngle.
468+
publicAPI.zoomToBoxUsingViewAngle = (box, ratioOrOffsetRatio = 1.0) => {
469+
let view = null;
470+
if (model._renderWindow && model._renderWindow.getViews) {
471+
const views = model._renderWindow.getViews();
472+
if (views.length > 0) {
473+
view = views[0];
474+
}
475+
}
476+
477+
if (!view || !view.getViewportSize) {
478+
return;
479+
}
480+
481+
const size = view.getViewportSize(publicAPI);
482+
if (!size || size[0] <= 0 || size[1] <= 0) {
483+
return;
484+
}
485+
486+
const zf1 = size[0] / box.width;
487+
const zf2 = size[1] / box.height;
488+
const zoomFactor = Math.min(zf1, zf2);
489+
publicAPI.getActiveCamera().zoom(zoomFactor * ratioOrOffsetRatio);
490+
};
491+
492+
// Port of VTK C++ vtkRenderer::ResetCameraScreenSpace.
493+
// Uses a screen-space bounding box to zoom closer to the data.
494+
publicAPI.resetCameraScreenSpace = (bounds = null, offsetRatio = 0.9) => {
495+
let effectiveBounds = bounds;
496+
let effectiveOffsetRatio = offsetRatio;
497+
if (typeof bounds === 'number') {
498+
effectiveOffsetRatio = bounds;
499+
effectiveBounds = null;
500+
}
501+
502+
const boundsToUse = effectiveBounds || publicAPI.computeVisiblePropBounds();
503+
504+
if (!vtkMath.areBoundsInitialized(boundsToUse)) {
505+
vtkDebugMacro('Cannot reset camera!');
506+
return false;
507+
}
508+
509+
// Make sure all bounds are visible to project into screen space
510+
publicAPI.resetCamera(boundsToUse);
511+
512+
// Expand bounds by camera model transform matrix
513+
const expandedBounds = [...boundsToUse];
514+
const modelTransformMatrix = publicAPI
515+
.getActiveCamera()
516+
.getModelTransformMatrix();
517+
if (modelTransformMatrix) {
518+
vtkBoundingBox.transformBounds(
519+
boundsToUse,
520+
modelTransformMatrix,
521+
expandedBounds
522+
);
523+
}
524+
525+
// Get the view from the render window to access viewport size
526+
let view = null;
527+
if (model._renderWindow && model._renderWindow.getViews) {
528+
const views = model._renderWindow.getViews();
529+
if (views.length > 0) {
530+
view = views[0];
531+
}
532+
}
533+
534+
if (!view || !view.getViewportSize) {
535+
return true;
536+
}
537+
538+
const size = view.getViewportSize(publicAPI);
539+
if (!size || size[0] <= 0 || size[1] <= 0) {
540+
return true;
541+
}
542+
543+
const aspect = size[0] / size[1];
544+
545+
// Compute the screen-space bounding box by projecting all 8 corners
546+
let xmin = Number.MAX_VALUE;
547+
let ymin = Number.MAX_VALUE;
548+
let xmax = -Number.MAX_VALUE;
549+
let ymax = -Number.MAX_VALUE;
550+
551+
for (let i = 0; i < 2; ++i) {
552+
for (let j = 0; j < 2; ++j) {
553+
for (let k = 0; k < 2; ++k) {
554+
const nd = publicAPI.worldToNormalizedDisplay(
555+
expandedBounds[i],
556+
expandedBounds[2 + j],
557+
expandedBounds[4 + k],
558+
aspect
559+
);
560+
const dx = nd[0] * size[0];
561+
const dy = nd[1] * size[1];
562+
xmin = Math.min(dx, xmin);
563+
xmax = Math.max(dx, xmax);
564+
ymin = Math.min(dy, ymin);
565+
ymax = Math.max(dy, ymax);
566+
}
567+
}
568+
}
569+
570+
// Project the focal point in screen space
571+
const fp = model.activeCamera.getFocalPoint();
572+
const fpNd = publicAPI.worldToNormalizedDisplay(
573+
fp[0],
574+
fp[1],
575+
fp[2],
576+
aspect
577+
);
578+
const fpDisplayX = fpNd[0] * size[0];
579+
const fpDisplayY = fpNd[1] * size[1];
580+
581+
// The focal point must be at the center of the box
582+
const xCenterFocalPoint = Math.trunc(fpDisplayX);
583+
const yCenterFocalPoint = Math.trunc(fpDisplayY);
584+
const xCenterBox = Math.trunc((xmin + xmax) / 2);
585+
const yCenterBox = Math.trunc((ymin + ymax) / 2);
586+
587+
const xDiff = 2 * (xCenterFocalPoint - xCenterBox);
588+
const yDiff = 2 * (yCenterFocalPoint - yCenterBox);
589+
590+
xmin += Math.min(xDiff, 0);
591+
xmax += Math.max(xDiff, 0);
592+
ymin += Math.min(yDiff, 0);
593+
ymax += Math.max(yDiff, 0);
594+
595+
const boxWidth = xmax - xmin;
596+
const boxHeight = ymax - ymin;
597+
598+
if (boxWidth > 0 && boxHeight > 0) {
599+
publicAPI.zoomToBoxUsingViewAngle(
600+
{ x: xmin, y: ymin, width: boxWidth, height: boxHeight },
601+
effectiveOffsetRatio
602+
);
603+
}
604+
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)