Skip to content

Commit 4ea50bd

Browse files
author
DavidQ
committed
Rework Input Mapping V2 layout and engine input model - PR_26140_097-rework-input-mapping-v2-layout-and-engine-input-model
1 parent d096bb5 commit 4ea50bd

19 files changed

Lines changed: 938 additions & 221 deletions
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Input Mapping V2 Layout Engine Model Report
2+
3+
## Scope
4+
- PR: `PR_26140_097-rework-input-mapping-v2-layout-and-engine-input-model`
5+
- Source of truth: user PR request. `docs/pr/BUILD_PR.md` was checked and is still the unrelated Level 18 overlay runtime hardening note.
6+
- Required setup: read `docs/dev/PROJECT_INSTRUCTIONS.md` first.
7+
8+
## Implementation
9+
- Reworked Input Mapping V2 into the requested left, center, and right column model.
10+
- Left column now contains Actions, Devices, Gestures, and Capture accordions.
11+
- Devices are rendered from engine-owned capability descriptors for Keyboard, Mouse, Game Controller, Touch, Pen, Wheel, Flight Stick, and VR Controller.
12+
- Gestures are rendered from engine-owned gesture descriptors and filtered by enabled devices.
13+
- Capture is limited to live capture controls: Capture Keyboard, Capture Mouse, and one button per connected game controller.
14+
- Center workspace keeps selected mapping tiles, token deletion, combo display, and now uses 225px by 225px tiles.
15+
- Right column now contains Diagnostics, Status / Log, and Export accordions.
16+
- Export and Copy JSON moved into the Export accordion and remain wired.
17+
- Added engine-level reusable capability descriptors plus wheel and pointer-drag gesture descriptors under `src/engine/input/**`.
18+
- Preserved pointer drag state support for down, move, up, drag, drag release, and drag rectangle/bounds.
19+
- Replaced visible Primary/Secondary mouse wording with human-friendly labels such as `Mouse Left Button`, `Mouse Middle Button`, and `Mouse Drag Rectangle`.
20+
- Added wheel input capture for combos such as `Shift + Mouse Wheel Up`.
21+
- Preserved game controller button naming, device-specific capture, title/hover detail, auto-polling, capture highlight/cancel/timeout, selected tile indication, and rumble WARN behavior.
22+
23+
## Files Changed
24+
- `src/engine/input/InputCapabilityDescriptors.js`
25+
- `src/engine/input/InputService.js`
26+
- `src/engine/input/PointerDragState.js`
27+
- `tests/input/InputService.test.mjs`
28+
- `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
29+
- `tools/input-mapping-v2/how_to_use.html`
30+
- `tools/input-mapping-v2/index.html`
31+
- `tools/input-mapping-v2/js/ToolStarterApp.js`
32+
- `tools/input-mapping-v2/js/bootstrap.js`
33+
- `tools/input-mapping-v2/js/controls/ActionNavControl.js`
34+
- `tools/input-mapping-v2/js/controls/ActionSelectionControl.js`
35+
- `tools/input-mapping-v2/js/controls/CaptureControl.js`
36+
- `tools/input-mapping-v2/js/controls/DeviceListControl.js`
37+
- `tools/input-mapping-v2/js/controls/ExportControl.js`
38+
- `tools/input-mapping-v2/js/controls/GamepadDiagnosticsControl.js`
39+
- `tools/input-mapping-v2/js/controls/GestureListControl.js`
40+
- `tools/input-mapping-v2/js/services/EngineInputSourceService.js`
41+
- `tools/input-mapping-v2/styles/inputMappingV2.css`
42+
43+
## Validation
44+
- Playwright impacted: Yes.
45+
- Targeted JS syntax validation for changed engine input, Input Mapping V2, and Playwright files: PASS.
46+
- Targeted engine input import/test run: PASS.
47+
- Targeted Input Mapping V2 browser import/page-load validation: PASS.
48+
- Focused Input Mapping V2 Playwright coverage: PASS, 2 tests.
49+
- `npm run test:workspace-v2`: PASS, 61 tests.
50+
- `git diff --check`: PASS, with repository line-ending warnings only.
51+
- Full samples smoke test: not run, per request and because sample JSON was out of scope.
52+
53+
## Playwright Coverage
54+
- Validates left/center/right accordion layout.
55+
- Validates Actions button order and created-action duplicate prevention.
56+
- Validates device list and gesture filtering by enabled devices.
57+
- Validates gamepad auto-polling and one capture button per detected controller.
58+
- Validates capture highlight, cancel, and timeout behavior.
59+
- Validates mouse drag rectangle gesture visibility and mapping.
60+
- Validates `Ctrl + R` and `Shift + Mouse Wheel Up` combo capture.
61+
- Validates 225px by 225px mapping tiles, tile selection, and token deletion.
62+
- Validates diagnostics cards fill the diagnostics scrollport width.
63+
- Validates mapping messages route to Status / Log, not Capture.
64+
- Validates Export and Copy JSON wiring.
65+
- Validates rumble checkbox and unsupported WARN.
66+
67+
## Manual Validation
68+
1. Open `tools/input-mapping-v2/index.html`.
69+
2. Confirm the left column accordions are Actions, Devices, Gestures, and Capture.
70+
3. Confirm the center column shows Captured Mappings tiles at 225px by 225px after adding an action.
71+
4. Confirm the right column accordions are Diagnostics, Status / Log, and Export.
72+
5. In Devices, toggle Wheel off and confirm wheel gestures disappear; toggle it back on and confirm they return.
73+
6. Select an action, click the Keyboard `Combo` gesture, press `Ctrl` then `R`.
74+
7. Expected: the selected mapping tile shows `Combo Ctrl + R` and the inspector contains `Combo:ControlLeft+KeyR`.
75+
8. Click the Mouse `Combo` gesture, press `Shift`, then scroll up.
76+
9. Expected: the selected mapping tile shows `Combo Shift + Mouse Wheel Up`.
77+
10. Connect or mock a controller and confirm one capture button appears for each detected game controller.
78+
11. Check `Gamepad rumble/haptic feedback` without a haptics-capable controller.
79+
12. Expected: Status / Log shows an actionable WARN and no fake rumble is attempted.
80+
13. Click Export and Copy JSON.
81+
14. Expected: Export shows toolState JSON in the Export accordion and Copy JSON logs success when clipboard is available.
82+
15. Out of scope: sample JSON edits and full samples smoke validation.
83+
84+
## Schema And Samples
85+
- No schema changes.
86+
- No sample JSON changes.
87+
88+
## Packaging
89+
- Delta ZIP: `tmp/PR_26140_097-rework-input-mapping-v2-layout-and-engine-input-model_delta.zip`
90+
- ZIP verification: PASS, 25 files, nonzero size.
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
05/22/2026
5+
InputCapabilityDescriptors.js
6+
*/
7+
8+
const DEVICE_DEFINITIONS = Object.freeze([
9+
{
10+
id: 'keyboard',
11+
label: 'Keyboard',
12+
engine: 'InputService + KeyboardState',
13+
supported: true,
14+
defaultEnabled: true,
15+
detail: 'Keyboard key states are captured from browser keydown and keyup events.'
16+
},
17+
{
18+
id: 'mouse',
19+
label: 'Mouse',
20+
engine: 'InputService + MouseState + PointerDragState',
21+
supported: true,
22+
defaultEnabled: true,
23+
detail: 'Mouse buttons, movement, and drag state are captured from browser mouse and pointer events.'
24+
},
25+
{
26+
id: 'gameController',
27+
label: 'Game Controller',
28+
engine: 'InputService + GamepadState + GamepadInputAdapter',
29+
supported: true,
30+
defaultEnabled: true,
31+
detail: 'Browser Gamepad API devices are auto-polled while the tool is active.'
32+
},
33+
{
34+
id: 'touch',
35+
label: 'Touch',
36+
engine: 'PointerEvent capability descriptor',
37+
supported: true,
38+
defaultEnabled: false,
39+
detail: 'Touch support depends on browser PointerEvent input. This tool exposes capability status only until touch capture is testable here.'
40+
},
41+
{
42+
id: 'pen',
43+
label: 'Pen',
44+
engine: 'PointerEvent capability descriptor',
45+
supported: true,
46+
defaultEnabled: false,
47+
detail: 'Pen support depends on browser PointerEvent input. This tool exposes capability status only until pen capture is testable here.'
48+
},
49+
{
50+
id: 'wheel',
51+
label: 'Wheel',
52+
engine: 'InputService wheel descriptor',
53+
supported: true,
54+
defaultEnabled: true,
55+
detail: 'Mouse wheel directions can be captured directly or as combo inputs.'
56+
},
57+
{
58+
id: 'flightStick',
59+
label: 'Flight Stick',
60+
engine: 'GamepadInputAdapter capability descriptor',
61+
supported: true,
62+
defaultEnabled: false,
63+
detail: 'Flight sticks are represented through the Gamepad API when the browser exposes them.'
64+
},
65+
{
66+
id: 'vrController',
67+
label: 'VR Controller',
68+
engine: 'WebXR capability descriptor',
69+
supported: false,
70+
defaultEnabled: false,
71+
detail: 'VR controller capture requires WebXR input sources and is not available in this tool context.'
72+
}
73+
]);
74+
75+
const GESTURE_DEFINITIONS = Object.freeze([
76+
keyboardGesture('KeyboardPress', 'Press', 'Keyboard key press', 'keyboard'),
77+
keyboardGesture('KeyboardRelease', 'Release', 'Keyboard key release', 'keyboard'),
78+
keyboardGesture('KeyboardHold', 'Hold', 'Keyboard key hold', 'keyboard'),
79+
comboGesture('KeyboardCombo', 'Keyboard', ['keyboard']),
80+
mouseGesture('MouseClick', 'Click', 'Mouse click', 'mouse'),
81+
mouseGesture('MouseDoubleClick', 'Double Click', 'Mouse double click', 'mouse'),
82+
pointerGesture('MousePrimaryDrag', 'Drag', 'Mouse drag', 'mouse'),
83+
pointerGesture('MousePrimaryDragRelease', 'Drag Release', 'Mouse drag release', 'mouse'),
84+
pointerGesture('MousePrimaryDragRectangle', 'Drag Rectangle', 'Mouse drag rectangle', 'mouse'),
85+
wheelGesture('MouseWheelUp', 'Wheel Up', 'Mouse wheel up', ['mouse', 'wheel']),
86+
wheelGesture('MouseWheelDown', 'Wheel Down', 'Mouse wheel down', ['mouse', 'wheel']),
87+
wheelGesture('MouseWheelLeft', 'Wheel Left', 'Mouse wheel left', ['mouse', 'wheel']),
88+
wheelGesture('MouseWheelRight', 'Wheel Right', 'Mouse wheel right', ['mouse', 'wheel']),
89+
comboGesture('MouseCombo', 'Mouse', ['mouse']),
90+
gameControllerGesture('GameControllerButton', 'Button', 'Game controller button', 'gameController'),
91+
gameControllerGesture('GameControllerTrigger', 'Trigger', 'Game controller trigger', 'gameController'),
92+
gameControllerGesture('GameControllerStick', 'Stick', 'Game controller stick', 'gameController'),
93+
gameControllerGesture('GameControllerDPad', 'DPad', 'Game controller DPad', 'gameController'),
94+
comboGesture('GameControllerCombo', 'Game Controller', ['gameController'])
95+
]);
96+
97+
export function inputDeviceCapabilities({
98+
gamepadCount = 0,
99+
gamepadWarning = '',
100+
pointerEventsAvailable = false,
101+
touchAvailable = false,
102+
penAvailable = false,
103+
wheelAvailable = true,
104+
webXrAvailable = false
105+
} = {}) {
106+
return DEVICE_DEFINITIONS.map((device) => {
107+
if (device.id === 'gameController') {
108+
return {
109+
...device,
110+
available: !gamepadWarning,
111+
detail: gamepadWarning || `${gamepadCount} connected game controller${gamepadCount === 1 ? '' : 's'} detected.`,
112+
emptyState: gamepadCount
113+
? ''
114+
: 'No game controllers are currently exposed. Click inside this page, press a controller button, and wait for auto-polling.'
115+
};
116+
}
117+
if (device.id === 'touch') {
118+
return {
119+
...device,
120+
available: pointerEventsAvailable && touchAvailable,
121+
emptyState: pointerEventsAvailable
122+
? 'Touch capability is exposed only when the browser reports a touch pointer.'
123+
: 'Touch capture requires browser PointerEvent support.'
124+
};
125+
}
126+
if (device.id === 'pen') {
127+
return {
128+
...device,
129+
available: pointerEventsAvailable && penAvailable,
130+
emptyState: pointerEventsAvailable
131+
? 'Pen capability is exposed only when the browser reports a pen pointer.'
132+
: 'Pen capture requires browser PointerEvent support.'
133+
};
134+
}
135+
if (device.id === 'wheel') {
136+
return {
137+
...device,
138+
available: wheelAvailable,
139+
emptyState: wheelAvailable ? '' : 'Wheel input requires browser wheel event support.'
140+
};
141+
}
142+
if (device.id === 'flightStick') {
143+
return {
144+
...device,
145+
available: !gamepadWarning,
146+
detail: gamepadWarning || device.detail,
147+
emptyState: gamepadCount
148+
? 'Use the game controller capture buttons for browser-exposed flight stick inputs.'
149+
: 'No browser-exposed flight stick is currently visible through the Gamepad API.'
150+
};
151+
}
152+
if (device.id === 'vrController') {
153+
return {
154+
...device,
155+
available: webXrAvailable,
156+
emptyState: webXrAvailable
157+
? 'WebXR is present, but this tool does not open an XR session for capture.'
158+
: device.detail
159+
};
160+
}
161+
return {
162+
...device,
163+
available: true,
164+
emptyState: ''
165+
};
166+
});
167+
}
168+
169+
export function inputGestureDescriptors({ enabledDeviceIds = [] } = {}) {
170+
const enabled = new Set(enabledDeviceIds);
171+
return GESTURE_DEFINITIONS.filter((gesture) => (
172+
gesture.requiredDeviceIds.every((deviceId) => enabled.has(deviceId))
173+
)).map((gesture) => ({ ...gesture }));
174+
}
175+
176+
export function getInputGestureDescriptor(binding, options = {}) {
177+
return inputGestureDescriptors(options).find((gesture) => gesture.binding === binding) ?? null;
178+
}
179+
180+
export function mouseButtonLabel(button) {
181+
const buttonNumber = Number(button);
182+
if (buttonNumber === 0) {
183+
return 'Mouse Left Button';
184+
}
185+
if (buttonNumber === 1) {
186+
return 'Mouse Middle Button';
187+
}
188+
if (buttonNumber === 2) {
189+
return 'Mouse Right Button';
190+
}
191+
if (buttonNumber === 3) {
192+
return 'Mouse Button 4';
193+
}
194+
if (buttonNumber === 4) {
195+
return 'Mouse Button 5';
196+
}
197+
return `Mouse Button ${buttonNumber + 1}`;
198+
}
199+
200+
export function wheelDescriptorFromEvent(event = {}) {
201+
const deltaX = Number(event.deltaX) || 0;
202+
const deltaY = Number(event.deltaY) || 0;
203+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
204+
return deltaX < 0
205+
? wheelInputDescriptor('MouseWheelLeft', 'Wheel Left')
206+
: wheelInputDescriptor('MouseWheelRight', 'Wheel Right');
207+
}
208+
return deltaY < 0
209+
? wheelInputDescriptor('MouseWheelUp', 'Wheel Up')
210+
: wheelInputDescriptor('MouseWheelDown', 'Wheel Down');
211+
}
212+
213+
export function wheelInputDescriptor(binding, detail) {
214+
return {
215+
source: 'mouse',
216+
binding,
217+
displayLabelLines: ['Mouse', detail],
218+
label: `Mouse ${detail}`,
219+
title: `Mouse\n${detail}`,
220+
engine: 'InputService Wheel'
221+
};
222+
}
223+
224+
function keyboardGesture(binding, label, title, deviceId) {
225+
return patternGesture({ binding, deviceLabel: 'Keyboard', label, title, deviceId });
226+
}
227+
228+
function mouseGesture(binding, label, title, deviceId) {
229+
return patternGesture({ binding, deviceLabel: 'Mouse', label, title, deviceId });
230+
}
231+
232+
function gameControllerGesture(binding, label, title, deviceId) {
233+
return patternGesture({ binding, deviceLabel: 'Game Controller', label, title, deviceId });
234+
}
235+
236+
function pointerGesture(binding, label, title, deviceId) {
237+
return {
238+
...patternGesture({ binding, deviceLabel: 'Mouse', label, title, deviceId }),
239+
captureKind: 'pointer-drag',
240+
source: 'mouse'
241+
};
242+
}
243+
244+
function wheelGesture(binding, label, title, requiredDeviceIds) {
245+
return {
246+
binding,
247+
captureKind: 'wheel',
248+
deviceLabel: 'Mouse',
249+
displayLabelLines: ['Mouse', label],
250+
engine: 'InputService Wheel',
251+
label,
252+
requiredDeviceIds,
253+
source: 'mouse',
254+
title
255+
};
256+
}
257+
258+
function comboGesture(binding, deviceLabel, requiredDeviceIds) {
259+
return {
260+
binding,
261+
captureKind: 'combo',
262+
deviceLabel,
263+
displayLabelLines: [deviceLabel, 'Combo'],
264+
engine: 'InputService Combo',
265+
label: 'Combo',
266+
requiredDeviceIds,
267+
source: requiredDeviceIds[0] === 'gameController' ? 'gamepad' : requiredDeviceIds[0],
268+
title: `${deviceLabel} combo`
269+
};
270+
}
271+
272+
function patternGesture({ binding, deviceLabel, label, title, deviceId }) {
273+
return {
274+
binding,
275+
captureKind: 'descriptor',
276+
deviceLabel,
277+
displayLabelLines: [deviceLabel, label],
278+
engine: 'InputService Capability',
279+
label,
280+
requiredDeviceIds: [deviceId],
281+
source: deviceId === 'gameController' ? 'gamepad' : deviceId,
282+
title
283+
};
284+
}

0 commit comments

Comments
 (0)