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