@@ -42,6 +42,7 @@ const SCROLLBAR_SEEK_SETTLE_MS = 64;
4242const SCROLLBAR_SEEK_EXPENSIVE_RENDER_MIN_MS = 32 ;
4343const SCROLLBAR_SEEK_EXPENSIVE_RENDER_FRAME_INTERVALS = 2 ;
4444const SCROLLBAR_SEEK_RENDER_COST_SAMPLE_WEIGHT = 0.25 ;
45+ const VIEWPORT_SHARED_EDGE_TOLERANCE = 1 ;
4546
4647export interface ISceneInputControlOptions {
4748 enableDown : boolean ;
@@ -108,6 +109,50 @@ function createScrollbarBounds(viewport: Viewport, contentBounds: IBoundRectNoAn
108109 return scrollbarBounds ;
109110}
110111
112+ // Viewports are rendered in order, so a later viewport owns any shared boundary pixel.
113+ // Excluding that pixel from earlier canvas copies prevents fixed headers from leaking into scrollable content.
114+ function trimSharedViewportEdges ( bounds : IBoundRectNoAngle , followingViewportBounds : IBoundRectNoAngle [ ] ) {
115+ const result = { ...bounds } ;
116+
117+ for ( const other of followingViewportBounds ) {
118+ const overlapWidth = Math . min ( result . right , other . right ) - Math . max ( result . left , other . left ) ;
119+ const overlapHeight = Math . min ( result . bottom , other . bottom ) - Math . max ( result . top , other . top ) ;
120+ if ( overlapWidth <= 0 || overlapHeight <= 0 ) {
121+ continue ;
122+ }
123+
124+ const width = result . right - result . left ;
125+ const height = result . bottom - result . top ;
126+ const coversWidth = overlapWidth >= width - VIEWPORT_SHARED_EDGE_TOLERANCE ;
127+ const coversHeight = overlapHeight >= height - VIEWPORT_SHARED_EDGE_TOLERANCE ;
128+
129+ if ( coversHeight && other . left <= result . left && other . right > result . left ) {
130+ result . left = Math . min ( other . right , result . right ) ;
131+ } else if ( coversHeight && other . left < result . right && other . right >= result . right ) {
132+ result . right = Math . max ( other . left , result . left ) ;
133+ }
134+
135+ if ( coversWidth && other . top <= result . top && other . bottom > result . top ) {
136+ result . top = Math . min ( other . bottom , result . bottom ) ;
137+ } else if ( coversWidth && other . top < result . bottom && other . bottom >= result . bottom ) {
138+ result . bottom = Math . max ( other . top , result . top ) ;
139+ }
140+ }
141+
142+ return result ;
143+ }
144+
145+ function isInvalidScrollBounds ( bounds : IBoundRectNoAngle , offsetX : number , offsetY : number ) {
146+ const width = bounds . right - bounds . left ;
147+ const height = bounds . bottom - bounds . top ;
148+ return ! Number . isFinite ( offsetX ) ||
149+ ! Number . isFinite ( offsetY ) ||
150+ width <= 0 ||
151+ height <= 0 ||
152+ Math . abs ( offsetX ) >= width ||
153+ Math . abs ( offsetY ) >= height ;
154+ }
155+
111156function createViewportScrollRenderState ( viewport : Viewport , scaleX : number , scaleY : number ) : IViewportScrollRenderState {
112157 const viewportInfo = viewport . calcViewportInfo ( ) ;
113158 const { diffX = 0 , diffY = 0 , viewPortPosition } = viewportInfo ;
@@ -128,24 +173,13 @@ function createViewportScrollRenderState(viewport: Viewport, scaleX: number, sca
128173 right : viewPortPosition . right - ( scrollBar ?. enableVertical ? scrollBar . totalSize : 0 ) ,
129174 bottom : viewPortPosition . bottom - ( scrollBar ?. enableHorizontal ? scrollBar . totalSize : 0 ) ,
130175 } ;
131- const width = bounds . right - bounds . left ;
132- const height = bounds . bottom - bounds . top ;
133- const isInvalidScroll = ! Number . isFinite ( offsetX ) ||
134- ! Number . isFinite ( offsetY ) ||
135- width <= 0 ||
136- height <= 0 ||
137- Math . abs ( offsetX ) >= width ||
138- Math . abs ( offsetY ) >= height ;
139- if ( isInvalidScroll ) {
176+ if ( isInvalidScrollBounds ( bounds , offsetX , offsetY ) ) {
140177 return { canPreserveEngine : false , dirtyBounds : [ ] , viewportInfo } ;
141178 }
142179
143180 return {
144181 canPreserveEngine : true ,
145- dirtyBounds : [
146- ...createExposedScrollBounds ( bounds , offsetX , offsetY ) ,
147- ...createScrollbarBounds ( viewport , bounds , viewPortPosition ) ,
148- ] ,
182+ dirtyBounds : createScrollbarBounds ( viewport , bounds , viewPortPosition ) ,
149183 scrollRenderInfo : { bounds, offsetX, offsetY } ,
150184 viewportInfo,
151185 } ;
@@ -880,21 +914,31 @@ export class Scene extends Disposable {
880914 const { scaleX, scaleY } = this . getAncestorScale ( ) ;
881915 let canPreserveEngine = true ;
882916
883- for ( const viewport of this . _viewports ) {
884- if ( ! viewport . shouldIntoRender ( ) ) {
885- continue ;
886- }
917+ const viewportStates = this . _viewports
918+ . filter ( ( viewport ) => viewport . shouldIntoRender ( ) )
919+ . map ( ( viewport ) => createViewportScrollRenderState ( viewport , scaleX , scaleY ) ) ;
887920
888- const viewportState = createViewportScrollRenderState ( viewport , scaleX , scaleY ) ;
921+ for ( const [ index , viewportState ] of viewportStates . entries ( ) ) {
889922 const { viewportInfo, scrollRenderInfo } = viewportState ;
890- viewportInfos . set ( viewport . viewportKey , viewportInfo ) ;
923+ viewportInfos . set ( viewportInfo . viewportKey , viewportInfo ) ;
891924 if ( ! viewportState . canPreserveEngine ) {
892925 canPreserveEngine = false ;
893926 continue ;
894927 }
895928 dirtyBounds . push ( ...viewportState . dirtyBounds ) ;
896929 if ( scrollRenderInfo ) {
897- scrollRenderInfos . push ( scrollRenderInfo ) ;
930+ const followingViewportBounds = viewportStates
931+ . slice ( index + 1 )
932+ . map ( ( state ) => state . viewportInfo . viewPortPosition )
933+ . filter ( ( bounds ) : bounds is IBoundRectNoAngle => bounds != null ) ;
934+ const bounds = trimSharedViewportEdges ( scrollRenderInfo . bounds , followingViewportBounds ) ;
935+ const { offsetX, offsetY } = scrollRenderInfo ;
936+ if ( isInvalidScrollBounds ( bounds , offsetX , offsetY ) ) {
937+ canPreserveEngine = false ;
938+ continue ;
939+ }
940+ dirtyBounds . push ( ...createExposedScrollBounds ( bounds , offsetX , offsetY ) ) ;
941+ scrollRenderInfos . push ( { bounds, offsetX, offsetY } ) ;
898942 }
899943 }
900944
0 commit comments