Skip to content

Commit 2f3b789

Browse files
author
DavidQ
committed
Add gesture support to overlay system.
PR Details: - Introduces gesture abstraction layer - Maps gestures to overlay interactions
1 parent 98815b9 commit 2f3b789

7 files changed

Lines changed: 390 additions & 18 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Implement advanced overlay interactions:
6-
- Add click/drag/resize
7-
- Maintain gameplay safety
5+
Implement gesture support for overlay system:
6+
- Add gesture abstraction (tap, hold, swipe)
7+
- Map gestures to overlay actions
8+
- Maintain compatibility with existing input system
89
- Update roadmap status only

docs/dev/COMMIT_COMMENT.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Add advanced overlay interactions.
1+
Add gesture support to overlay system.
22

33
PR Details:
4-
- Introduces interactive overlay elements
4+
- Introduces gesture abstraction layer
5+
- Maps gestures to overlay interactions
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
[ ] Interaction works
2-
[ ] No gameplay interference
3-
[ ] Stable behavior
1+
[ ] Tap gesture works
2+
[ ] Hold gesture works
3+
[ ] Swipe gesture works
4+
[ ] No regression in keyboard/mouse input
45
[ ] Roadmap updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@
635635
- [x] Level 18 overlay system baseline promoted after validation (input, mission, telemetry integration; no Level 17/18 overlay regressions)
636636
- [x] Level 21 overlay system baseline promoted after validation (context-aware behavior, synchronized state, event-driven updates, and performance optimization; no Level 17/19 overlay regressions)
637637
- [x] Level 21 advanced overlay interactions implemented (click/drag/resize) with gameplay-safe explicit interaction mode
638+
- [x] Level 22 gesture abstraction added for overlays (tap/hold/swipe mapped to runtime actions with explicit gameplay-safe gating)
638639

639640
### Sample Phase Tracks
640641
- [x] 3D phase normalized

docs/pr/BUILD_PR.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
# BUILD_PR_LEVEL_22_1_OVERLAY_ADVANCED_INTERACTIONS
1+
# BUILD_PR_LEVEL_22_2_OVERLAY_GESTURE_SUPPORT
22

33
## Purpose
4-
Introduce advanced user interactions with overlays.
4+
Extend overlay interaction system with gesture-based input support.
55

66
## Roadmap Improvement
7-
Begins Level 22 interaction enhancements.
7+
Adds gesture layer on top of interaction system (Level 22).
88

99
## Scope
10-
- Add interactive elements (click, drag, resize)
11-
- Ensure gameplay compatibility
10+
- Add gesture abstraction (tap, hold, swipe)
11+
- Map gestures to overlay actions
12+
- Ensure compatibility with keyboard/mouse input
1213

1314
## Test Steps
14-
1. Interact with overlay elements
15-
2. Verify behavior
16-
3. Confirm gameplay unaffected
15+
1. Perform tap/hold/swipe on overlays
16+
2. Validate correct mapping
17+
3. Ensure no regression in keyboard controls
1718

1819
## Expected
19-
- Interactive overlays
20-
- Stable system
20+
- Gesture support operational
21+
- No input conflicts

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
11261325
export function stepOverlayGameplayRuntimePointerInteractions(runtime, pointerState = {}, options = {}) {
11271326
if (!runtime) {
11281327
return {

0 commit comments

Comments
 (0)