@@ -901,6 +901,8 @@ export function createOverlayGameplayRuntime({ runtimeExtensions = [] } = {}) {
901901 interactionSelectedLayoutKey : '' ,
902902 interactionPointerDragState : null ,
903903 interactionPointerLastDown : false ,
904+ interactionGestureState : null ,
905+ interactionGestureLastDown : false ,
904906 } ;
905907}
906908
@@ -1123,6 +1125,203 @@ function clampOverlayLayoutRect(rect, canvasWidth, canvasHeight, minPanelWidth,
11231125 } ;
11241126}
11251127
1128+ function normalizeGesturePointerState ( pointerState = { } , runtime = null ) {
1129+ const down = pointerState ?. down === true ;
1130+ const previousDown = runtime ?. interactionGestureLastDown === true ;
1131+ const pressed = pointerState ?. pressed === true || ( down && ! previousDown ) ;
1132+ const released = pointerState ?. released === true || ( ! down && previousDown ) ;
1133+ const x = normalizePointerNumber ( pointerState ?. x , - 1 ) ;
1134+ const y = normalizePointerNumber ( pointerState ?. y , - 1 ) ;
1135+ const modifiers = pointerState ?. modifiers && typeof pointerState . modifiers === 'object'
1136+ ? pointerState . modifiers
1137+ : { } ;
1138+ return {
1139+ x,
1140+ y,
1141+ down,
1142+ pressed,
1143+ released,
1144+ modifiers : {
1145+ shift : modifiers . shift === true ,
1146+ alt : modifiers . alt === true ,
1147+ ctrl : modifiers . ctrl === true ,
1148+ meta : modifiers . meta === true ,
1149+ } ,
1150+ } ;
1151+ }
1152+
1153+ function resolveSwipeDirection ( dx , dy ) {
1154+ const absX = Math . abs ( dx ) ;
1155+ const absY = Math . abs ( dy ) ;
1156+ if ( absX >= absY ) {
1157+ return dx >= 0 ? 'right' : 'left' ;
1158+ }
1159+ return dy >= 0 ? 'down' : 'up' ;
1160+ }
1161+
1162+ function mapGestureToOverlayAction ( gesture , direction = '' ) {
1163+ if ( gesture === 'hold' ) {
1164+ return 'toggle-visibility' ;
1165+ }
1166+ if ( gesture === 'swipe' ) {
1167+ if ( direction === 'left' || direction === 'up' ) {
1168+ return 'cycle-prev' ;
1169+ }
1170+ return 'cycle-next' ;
1171+ }
1172+ if ( gesture === 'tap' ) {
1173+ return 'cycle-next' ;
1174+ }
1175+ return '' ;
1176+ }
1177+
1178+ function applyOverlayGestureAction ( runtime , action ) {
1179+ if ( ! runtime ) {
1180+ return false ;
1181+ }
1182+ if ( action === 'toggle-visibility' ) {
1183+ runtime . interactionVisible = runtime . interactionVisible === false ;
1184+ return true ;
1185+ }
1186+ if ( action === 'cycle-next' || action === 'cycle-prev' ) {
1187+ if ( ! Array . isArray ( runtime . runtimeExtensions ) || runtime . runtimeExtensions . length <= 1 ) {
1188+ return false ;
1189+ }
1190+ normalizeInteractionIndex ( runtime ) ;
1191+ const count = runtime . runtimeExtensions . length ;
1192+ const delta = action === 'cycle-prev' ? - 1 : 1 ;
1193+ runtime . interactionIndex = ( runtime . interactionIndex + delta + count ) % count ;
1194+ return true ;
1195+ }
1196+ return false ;
1197+ }
1198+
1199+ export function stepOverlayGameplayRuntimeGestures ( runtime , pointerState = { } , options = { } ) {
1200+ if ( ! runtime ) {
1201+ return {
1202+ gesture : '' ,
1203+ action : '' ,
1204+ direction : '' ,
1205+ consumed : false ,
1206+ changed : false ,
1207+ } ;
1208+ }
1209+
1210+ const pointer = normalizeGesturePointerState ( pointerState , runtime ) ;
1211+ runtime . interactionGestureLastDown = pointer . down ;
1212+ const result = {
1213+ gesture : '' ,
1214+ action : '' ,
1215+ direction : '' ,
1216+ consumed : false ,
1217+ changed : false ,
1218+ } ;
1219+
1220+ if ( options ?. enableGestures !== true ) {
1221+ if ( ! pointer . down ) {
1222+ runtime . interactionGestureState = null ;
1223+ }
1224+ return result ;
1225+ }
1226+
1227+ const requireModifier = options ?. requireModifier !== false ;
1228+ const modifierActive = requireModifier
1229+ ? ( pointer . modifiers . alt === true && pointer . modifiers . shift === true )
1230+ : true ;
1231+ const hasActiveGesture = runtime . interactionGestureState && typeof runtime . interactionGestureState === 'object' ;
1232+ if ( ! modifierActive && ! hasActiveGesture ) {
1233+ if ( ! pointer . down ) {
1234+ runtime . interactionGestureState = null ;
1235+ }
1236+ return result ;
1237+ }
1238+
1239+ const tapMaxSeconds = Math . max ( 0.05 , Number ( options ?. tapMaxSeconds ) || 0.25 ) ;
1240+ const tapMaxDistance = Math . max ( 4 , Number ( options ?. tapMaxDistance ) || 18 ) ;
1241+ const holdMinSeconds = Math . max ( 0.1 , Number ( options ?. holdMinSeconds ) || 0.3 ) ;
1242+ const holdMoveTolerance = Math . max ( 4 , Number ( options ?. holdMoveTolerance ) || 14 ) ;
1243+ const swipeMinDistance = Math . max ( 16 , Number ( options ?. swipeMinDistance ) || 48 ) ;
1244+ const dtSeconds = Math . max ( 0 , Math . min ( 0.25 , Number ( options ?. dtSeconds ) || 0.016 ) ) ;
1245+
1246+ if ( pointer . pressed ) {
1247+ runtime . interactionGestureState = {
1248+ startX : pointer . x ,
1249+ startY : pointer . y ,
1250+ lastX : pointer . x ,
1251+ lastY : pointer . y ,
1252+ elapsedSeconds : 0 ,
1253+ maxDistance : 0 ,
1254+ holdTriggered : false ,
1255+ } ;
1256+ }
1257+
1258+ const gestureState = runtime . interactionGestureState ;
1259+ if ( gestureState && pointer . down ) {
1260+ const dx = pointer . x - normalizePointerNumber ( gestureState . startX , pointer . x ) ;
1261+ const dy = pointer . y - normalizePointerNumber ( gestureState . startY , pointer . y ) ;
1262+ const distance = Math . sqrt ( ( dx * dx ) + ( dy * dy ) ) ;
1263+ gestureState . lastX = pointer . x ;
1264+ gestureState . lastY = pointer . y ;
1265+ gestureState . elapsedSeconds = Math . max ( 0 , Number ( gestureState . elapsedSeconds ) || 0 ) + dtSeconds ;
1266+ gestureState . maxDistance = Math . max ( Number ( gestureState . maxDistance ) || 0 , distance ) ;
1267+
1268+ if ( ! gestureState . holdTriggered && gestureState . elapsedSeconds >= holdMinSeconds && gestureState . maxDistance <= holdMoveTolerance ) {
1269+ const action = mapGestureToOverlayAction ( 'hold' ) ;
1270+ const changed = applyOverlayGestureAction ( runtime , action ) ;
1271+ gestureState . holdTriggered = true ;
1272+ result . gesture = 'hold' ;
1273+ result . action = action ;
1274+ result . consumed = true ;
1275+ result . changed = changed ;
1276+ return result ;
1277+ }
1278+ }
1279+
1280+ if ( gestureState && pointer . released ) {
1281+ const dx = pointer . x - normalizePointerNumber ( gestureState . startX , pointer . x ) ;
1282+ const dy = pointer . y - normalizePointerNumber ( gestureState . startY , pointer . y ) ;
1283+ const distance = Math . sqrt ( ( dx * dx ) + ( dy * dy ) ) ;
1284+ const elapsedSeconds = Math . max ( 0 , Number ( gestureState . elapsedSeconds ) || 0 ) ;
1285+
1286+ runtime . interactionGestureState = null ;
1287+ if ( gestureState . holdTriggered ) {
1288+ result . gesture = 'hold' ;
1289+ result . action = mapGestureToOverlayAction ( 'hold' ) ;
1290+ result . consumed = true ;
1291+ result . changed = false ;
1292+ return result ;
1293+ }
1294+
1295+ if ( distance <= tapMaxDistance && elapsedSeconds <= tapMaxSeconds ) {
1296+ const action = mapGestureToOverlayAction ( 'tap' ) ;
1297+ const changed = applyOverlayGestureAction ( runtime , action ) ;
1298+ result . gesture = 'tap' ;
1299+ result . action = action ;
1300+ result . consumed = true ;
1301+ result . changed = changed ;
1302+ return result ;
1303+ }
1304+
1305+ if ( distance >= swipeMinDistance ) {
1306+ const direction = resolveSwipeDirection ( dx , dy ) ;
1307+ const action = mapGestureToOverlayAction ( 'swipe' , direction ) ;
1308+ const changed = applyOverlayGestureAction ( runtime , action ) ;
1309+ result . gesture = 'swipe' ;
1310+ result . action = action ;
1311+ result . direction = direction ;
1312+ result . consumed = true ;
1313+ result . changed = changed ;
1314+ return result ;
1315+ }
1316+ }
1317+
1318+ if ( ! pointer . down && ! pointer . released ) {
1319+ runtime . interactionGestureState = null ;
1320+ }
1321+
1322+ return result ;
1323+ }
1324+
11261325export function stepOverlayGameplayRuntimePointerInteractions ( runtime , pointerState = { } , options = { } ) {
11271326 if ( ! runtime ) {
11281327 return {
0 commit comments