@@ -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