@@ -104,6 +104,12 @@ function vtkRenderWindowInteractor(publicAPI, model) {
104104 // Factor to apply on wheel spin.
105105 let wheelCoefficient = 1 ;
106106
107+ // Track mouse button bitmask for detecting chorded button interactions.
108+ // Per W3C Pointer Events spec §10, pointerdown/pointerup only fire for the
109+ // first press / last release. Chorded (additional) button changes while
110+ // another button is held are signaled via pointermove with updated `buttons`.
111+ let previousMouseButtons = 0 ;
112+
107113 // Public API methods
108114
109115 //----------------------------------------------------------------------
@@ -321,6 +327,7 @@ function vtkRenderWindowInteractor(publicAPI, model) {
321327 publicAPI . handlePointerLockChange
322328 ) ;
323329 pointerCache . clear ( ) ;
330+ previousMouseButtons = 0 ;
324331 } ;
325332
326333 publicAPI . unbindEvents = ( ) => {
@@ -398,6 +405,7 @@ function vtkRenderWindowInteractor(publicAPI, model) {
398405 case 'mouse' :
399406 default :
400407 publicAPI . handleMouseDown ( event ) ;
408+ previousMouseButtons = event . buttons ;
401409 break ;
402410 }
403411 } ;
@@ -417,9 +425,36 @@ function vtkRenderWindowInteractor(publicAPI, model) {
417425 publicAPI . handleTouchEnd ( event ) ;
418426 break ;
419427 case 'mouse' :
420- default :
428+ default : {
429+ // Detect chorded button releases: when pointerup fires, additional
430+ // buttons may have been released simultaneously. Fire release events
431+ // for those chorded buttons before handling the primary button.
432+ const currentButtons = event . buttons ;
433+ if ( currentButtons !== previousMouseButtons ) {
434+ const callData = {
435+ ...getModifierKeysFor ( event ) ,
436+ position : getScreenEventPositionFor ( event ) ,
437+ deviceType : getDeviceTypeFor ( event ) ,
438+ } ;
439+ const wasLeft = ( previousMouseButtons & 1 ) !== 0 ; // eslint-disable-line no-bitwise
440+ const wasRight = ( previousMouseButtons & 2 ) !== 0 ; // eslint-disable-line no-bitwise
441+ const wasMiddle = ( previousMouseButtons & 4 ) !== 0 ; // eslint-disable-line no-bitwise
442+ const isLeft = ( currentButtons & 1 ) !== 0 ; // eslint-disable-line no-bitwise
443+ const isRight = ( currentButtons & 2 ) !== 0 ; // eslint-disable-line no-bitwise
444+ const isMiddle = ( currentButtons & 4 ) !== 0 ; // eslint-disable-line no-bitwise
445+ // Only fire for chorded buttons; the primary button (event.button)
446+ // is handled by handleMouseUp below.
447+ if ( ! isLeft && wasLeft && event . button !== 0 )
448+ publicAPI . leftButtonReleaseEvent ( callData ) ;
449+ if ( ! isMiddle && wasMiddle && event . button !== 1 )
450+ publicAPI . middleButtonReleaseEvent ( callData ) ;
451+ if ( ! isRight && wasRight && event . button !== 2 )
452+ publicAPI . rightButtonReleaseEvent ( callData ) ;
453+ }
421454 publicAPI . handleMouseUp ( event ) ;
455+ previousMouseButtons = currentButtons ;
422456 break ;
457+ }
423458 }
424459 }
425460 } ;
@@ -434,9 +469,26 @@ function vtkRenderWindowInteractor(publicAPI, model) {
434469 publicAPI . handleTouchEnd ( event ) ;
435470 break ;
436471 case 'mouse' :
437- default :
438- publicAPI . handleMouseUp ( event ) ;
472+ default : {
473+ // Fire release events for all buttons that were held when the
474+ // pointer interaction was cancelled.
475+ const callData = {
476+ ...getModifierKeysFor ( event ) ,
477+ position : getScreenEventPositionFor ( event ) ,
478+ deviceType : getDeviceTypeFor ( event ) ,
479+ } ;
480+ // eslint-disable-next-line no-bitwise
481+ if ( ( previousMouseButtons & 1 ) !== 0 )
482+ publicAPI . leftButtonReleaseEvent ( callData ) ;
483+ // eslint-disable-next-line no-bitwise
484+ if ( ( previousMouseButtons & 4 ) !== 0 )
485+ publicAPI . middleButtonReleaseEvent ( callData ) ;
486+ // eslint-disable-next-line no-bitwise
487+ if ( ( previousMouseButtons & 2 ) !== 0 )
488+ publicAPI . rightButtonReleaseEvent ( callData ) ;
489+ previousMouseButtons = 0 ;
439490 break ;
491+ }
440492 }
441493 }
442494 } ;
@@ -453,9 +505,38 @@ function vtkRenderWindowInteractor(publicAPI, model) {
453505 publicAPI . handleTouchMove ( event ) ;
454506 break ;
455507 case 'mouse' :
456- default :
508+ default : {
509+ // Detect chorded button state changes (W3C Pointer Events spec §10).
510+ // pointerdown/pointerup only fire for the first/last button; additional
511+ // button presses/releases while another is held arrive as pointermove
512+ // events with updated `buttons` bitmask.
513+ const currentButtons = event . buttons ;
514+ if ( currentButtons !== previousMouseButtons ) {
515+ const callData = {
516+ ...getModifierKeysFor ( event ) ,
517+ position : getScreenEventPositionFor ( event ) ,
518+ deviceType : getDeviceTypeFor ( event ) ,
519+ } ;
520+ // buttons bitmask: 1=left, 2=right, 4=middle
521+ const wasLeft = ( previousMouseButtons & 1 ) !== 0 ; // eslint-disable-line no-bitwise
522+ const wasRight = ( previousMouseButtons & 2 ) !== 0 ; // eslint-disable-line no-bitwise
523+ const wasMiddle = ( previousMouseButtons & 4 ) !== 0 ; // eslint-disable-line no-bitwise
524+ const isLeft = ( currentButtons & 1 ) !== 0 ; // eslint-disable-line no-bitwise
525+ const isRight = ( currentButtons & 2 ) !== 0 ; // eslint-disable-line no-bitwise
526+ const isMiddle = ( currentButtons & 4 ) !== 0 ; // eslint-disable-line no-bitwise
527+ if ( isLeft && ! wasLeft ) publicAPI . leftButtonPressEvent ( callData ) ;
528+ if ( isMiddle && ! wasMiddle )
529+ publicAPI . middleButtonPressEvent ( callData ) ;
530+ if ( isRight && ! wasRight ) publicAPI . rightButtonPressEvent ( callData ) ;
531+ if ( ! isLeft && wasLeft ) publicAPI . leftButtonReleaseEvent ( callData ) ;
532+ if ( ! isMiddle && wasMiddle )
533+ publicAPI . middleButtonReleaseEvent ( callData ) ;
534+ if ( ! isRight && wasRight ) publicAPI . rightButtonReleaseEvent ( callData ) ;
535+ previousMouseButtons = currentButtons ;
536+ }
457537 publicAPI . handleMouseMove ( event ) ;
458538 break ;
539+ }
459540 }
460541 } ;
461542
0 commit comments