Skip to content

Commit f148cbe

Browse files
committed
Polish Input Mapping V2 device usage and tile actions - PR_26140_101-polish-input-mapping-v2-device-usage-and-tile-actions
1 parent 9a1c339 commit f148cbe

14 files changed

Lines changed: 317 additions & 79 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Input Mapping V2 Device Usage And Tile Actions Report
2+
3+
## Scope
4+
- PR: PR_26140_101-polish-input-mapping-v2-device-usage-and-tile-actions
5+
- Source of truth: user PR_101 request. `docs/pr/BUILD_PR.md` still points at an unrelated Level 18 overlay runtime hardening rebase, so this workflow used the explicit PR_101 request after reading `docs/dev/PROJECT_INSTRUCTIONS.md`.
6+
7+
## Changes Applied
8+
- Moved the existing gamepad rumble/haptic checkbox into the `Game Controller` device card.
9+
- Removed `Wheel` as a separate visible device and treats wheel directions as Mouse gestures.
10+
- Kept `Mouse Wheel Up`, `Mouse Wheel Down`, `Mouse Wheel Left`, and `Mouse Wheel Right` visible when Mouse is enabled and browser wheel support or advanced input mode is available.
11+
- Moved `inputMappingV2ClearActionButton` into the Captured Mappings area, renamed it to `Delete All`, and wired it to delete all visible captured mapping tiles/actions.
12+
- Kept the Actions accordion to selected-action creation/deletion only: `Add`, `Delete`.
13+
- Added idle selected-action source highlighting for already-used Keyboard, Mouse, and visible Game Controller controls.
14+
- Added gesture text to captured mapping tile tokens while preserving concise labels and detailed title/hover metadata.
15+
- Preserved combo display, selected tile indication, capture highlight/cancel behavior, gamepad auto-polling, and 225x225 tiles.
16+
17+
## Contracts And Constraints
18+
- No schema changes.
19+
- No sample JSON changes.
20+
- No full samples smoke test; skipped per request because this is scoped to Input Mapping V2 and engine input descriptors.
21+
- Rumble/haptic support remains UI-local because the existing Input Mapping V2 toolState contract has no options field.
22+
23+
## Validation
24+
- `node --check src/engine/input/InputCapabilityDescriptors.js`: PASS
25+
- `node --check tools/input-mapping-v2/js/bootstrap.js`: PASS
26+
- `node --check tools/input-mapping-v2/js/ToolStarterApp.js`: PASS
27+
- `node --check tools/input-mapping-v2/js/controls/ActionSelectionControl.js`: PASS
28+
- `node --check tools/input-mapping-v2/js/controls/CaptureControl.js`: PASS
29+
- `node --check tools/input-mapping-v2/js/controls/DeviceListControl.js`: PASS
30+
- `node --check tools/input-mapping-v2/js/controls/PreviewPanelControl.js`: PASS
31+
- `node --check tools/input-mapping-v2/js/services/EngineInputSourceService.js`: PASS
32+
- `node --check tools/input-mapping-v2/js/services/InputMappingState.js`: PASS
33+
- `node --check tests/input/InputService.test.mjs`: PASS
34+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`: PASS
35+
- `node -e "const test = await import('./tests/input/InputService.test.mjs'); test.run();"`: PASS
36+
- Focused Input Mapping V2 Playwright: PASS, 2 tests
37+
- `npm run test:workspace-v2`: PASS, 61 tests
38+
- `git diff --check`: PASS, line-ending warnings only
39+
- HTML inline script/style/handler scan: PASS, no matches
40+
- Sample/JSON diff scan: PASS, no changed sample or JSON files
41+
- Playwright V8 coverage report: PASS, changed runtime JS listed in `docs/dev/reports/playwright_v8_coverage_report.txt` and `coverage_changed_js_guardrail.txt`
42+
- Full samples smoke test: not run, per request
43+
- Delta ZIP verification: PASS, 20 files, nonzero size
44+
45+
## Playwright Coverage
46+
- Confirms rumble/haptics appears inside the Game Controller device card.
47+
- Confirms Wheel is not a separate device.
48+
- Confirms wheel gestures appear inside Mouse and hide when Mouse is disabled.
49+
- Confirms `Delete All` appears at the bottom of Captured Mappings and clears captured mappings.
50+
- Confirms selected-action used Keyboard, Mouse, and Game Controller controls are highlighted outside capture mode.
51+
- Confirms mapping tiles display gesture text for keyboard, mouse, drag release, gamepad, and combo inputs.
52+
- Confirms combo display, selected tile behavior, gamepad auto-polling, and capture highlight/cancel behavior remain intact.
53+
54+
## Manual Validation
55+
1. Open `tools/input-mapping-v2/index.html`.
56+
2. In Devices, confirm there is no separate Wheel card and `Gamepad rumble/haptic feedback` is inside Game Controller.
57+
3. In Gestures, confirm wheel directions appear under Mouse.
58+
4. Capture keyboard, mouse, drag release, and game controller inputs; confirm the selected action highlights the used source controls while capture is idle.
59+
5. Confirm captured mapping tokens include gesture text such as `Press`, `Click`, `Drag Release`, or `Button`.
60+
6. Click `Delete All` at the bottom of Captured Mappings and confirm all mapping tiles are removed.

src/engine/input/InputCapabilityDescriptors.js

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,6 @@ const DEVICE_DEFINITIONS = Object.freeze([
4646
defaultEnabled: false,
4747
detail: 'Pen support depends on browser PointerEvent input. This tool exposes capability status only until pen capture is testable here.'
4848
},
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-
},
5749
{
5850
id: 'flightStick',
5951
label: 'Flight Stick',
@@ -81,10 +73,10 @@ const GESTURE_DEFINITIONS = Object.freeze([
8173
mouseGesture('MouseDoubleClick', 'Double Click', 'Mouse double click', 'mouse'),
8274
pointerGesture('MousePrimaryDrag', 'Drag', 'Mouse drag', 'mouse'),
8375
pointerGesture('MousePrimaryDragRelease', 'Drag Release', 'Mouse drag release', 'mouse'),
84-
wheelGesture('MouseWheelUp', 'Wheel Up', 'Mouse wheel up', ['mouse', 'wheel']),
85-
wheelGesture('MouseWheelDown', 'Wheel Down', 'Mouse wheel down', ['mouse', 'wheel']),
86-
wheelGesture('MouseWheelLeft', 'Wheel Left', 'Mouse wheel left', ['mouse', 'wheel']),
87-
wheelGesture('MouseWheelRight', 'Wheel Right', 'Mouse wheel right', ['mouse', 'wheel']),
76+
wheelGesture('MouseWheelUp', 'Wheel Up', 'Mouse wheel up'),
77+
wheelGesture('MouseWheelDown', 'Wheel Down', 'Mouse wheel down'),
78+
wheelGesture('MouseWheelLeft', 'Wheel Left', 'Mouse wheel left'),
79+
wheelGesture('MouseWheelRight', 'Wheel Right', 'Mouse wheel right'),
8880
comboGesture('MouseCombo', 'Mouse', ['mouse']),
8981
gameControllerGesture('GameControllerButton', 'Button', 'Game controller button', 'gameController'),
9082
gameControllerGesture('GameControllerTrigger', 'Trigger', 'Game controller trigger', 'gameController'),
@@ -99,7 +91,6 @@ export function inputDeviceCapabilities({
9991
pointerEventsAvailable = false,
10092
touchAvailable = false,
10193
penAvailable = false,
102-
wheelAvailable = true,
10394
webXrAvailable = false
10495
} = {}) {
10596
return DEVICE_DEFINITIONS.map((device) => {
@@ -131,13 +122,6 @@ export function inputDeviceCapabilities({
131122
: 'Pen capture requires browser PointerEvent support.'
132123
};
133124
}
134-
if (device.id === 'wheel') {
135-
return {
136-
...device,
137-
available: wheelAvailable,
138-
emptyState: wheelAvailable ? '' : 'Wheel input requires browser wheel event support.'
139-
};
140-
}
141125
if (device.id === 'flightStick') {
142126
return {
143127
...device,
@@ -165,10 +149,15 @@ export function inputDeviceCapabilities({
165149
});
166150
}
167151

168-
export function inputGestureDescriptors({ enabledDeviceIds = [] } = {}) {
152+
export function inputGestureDescriptors({
153+
advancedModeAvailable = false,
154+
enabledDeviceIds = [],
155+
wheelAvailable = true
156+
} = {}) {
169157
const enabled = new Set(enabledDeviceIds);
170158
return GESTURE_DEFINITIONS.filter((gesture) => (
171159
gesture.requiredDeviceIds.every((deviceId) => enabled.has(deviceId))
160+
&& (gesture.captureKind !== 'wheel' || wheelAvailable || advancedModeAvailable)
172161
)).map((gesture) => ({ ...gesture }));
173162
}
174163

@@ -213,7 +202,7 @@ export function wheelInputDescriptor(binding, detail) {
213202
return {
214203
source: 'mouse',
215204
binding,
216-
displayLabelLines: ['Mouse', detail],
205+
displayLabelLines: ['Mouse', detail, 'Wheel'],
217206
label: `Mouse ${detail}`,
218207
title: `Mouse\n${detail}`,
219208
engine: 'InputService Wheel'
@@ -240,15 +229,15 @@ function pointerGesture(binding, label, title, deviceId) {
240229
};
241230
}
242231

243-
function wheelGesture(binding, label, title, requiredDeviceIds) {
232+
function wheelGesture(binding, label, title) {
244233
return {
245234
binding,
246235
captureKind: 'wheel',
247236
deviceLabel: 'Mouse',
248237
displayLabelLines: ['Mouse', label],
249238
engine: 'InputService Wheel',
250239
label,
251-
requiredDeviceIds,
240+
requiredDeviceIds: ['mouse'],
252241
source: 'mouse',
253242
title
254243
};

tests/input/InputService.test.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@ export function run() {
8282
assert(dragRelease.snapshot.dragBounds.height === 40, 'Drag release descriptors should carry drag bounds height.');
8383
const capabilities = input.getInputDeviceCapabilities({ gamepadCount: 0 });
8484
assert(capabilities.some((device) => device.label === 'VR Controller'), 'Input capabilities should expose safe VR controller descriptors.');
85-
const gestures = input.getInputGestureDescriptors({ enabledDeviceIds: ['keyboard', 'mouse', 'wheel'] });
86-
assert(gestures.some((gesture) => gesture.binding === 'MouseWheelUp'), 'Input gestures should expose wheel descriptors when wheel is enabled.');
85+
assert(!capabilities.some((device) => device.label === 'Wheel'), 'Input capabilities should treat wheel as mouse input instead of a device.');
86+
const gestures = input.getInputGestureDescriptors({ enabledDeviceIds: ['keyboard', 'mouse'] });
87+
assert(gestures.some((gesture) => gesture.binding === 'MouseWheelUp'), 'Input gestures should expose wheel descriptors through Mouse when wheel is available.');
88+
const gesturesWithoutWheelSupport = input.getInputGestureDescriptors({ enabledDeviceIds: ['keyboard', 'mouse'], wheelAvailable: false });
89+
assert(!gesturesWithoutWheelSupport.some((gesture) => gesture.binding === 'MouseWheelUp'), 'Input gestures should hide wheel descriptors when wheel support is unavailable.');
8790
assert(!gestures.some((gesture) => gesture.binding === 'MousePrimaryDragRectangle'), 'Visible input gestures should not expose Drag Rectangle.');
8891

8992
input.detach();

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,10 +1507,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15071507
"Game Controller",
15081508
"Touch",
15091509
"Pen",
1510-
"Wheel",
15111510
"Flight Stick",
15121511
"VR Controller"
15131512
]);
1513+
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='wheel']")).toHaveCount(0);
15141514
await expect(page.locator("#inputMappingV2DeviceList")).toContainText("InputService + KeyboardState");
15151515
await expect(page.locator("#inputMappingV2DeviceList")).toContainText("InputService + MouseState + PointerDragState");
15161516
await expect(page.locator("#inputMappingV2DeviceList")).toContainText("InputService + GamepadState + GamepadInputAdapter");
@@ -1523,6 +1523,9 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15231523
await expect(page.locator("#inputMappingV2GestureList")).not.toContainText("Drag Rectangle");
15241524
await expect(page.locator("#inputMappingV2GestureList")).not.toContainText(/Mouse Move [XY]/);
15251525
await expect(page.locator("#inputMappingV2GestureList")).toContainText("Button");
1526+
await expect(page.locator(".input-mapping-v2__gesture-group", { hasText: "Mouse" })).toContainText("Wheel Up");
1527+
const gestureGroupTitles = await page.locator(".input-mapping-v2__gesture-group strong").allTextContents();
1528+
expect(gestureGroupTitles).not.toContain("Wheel");
15261529
const compactAccordionSpacing = await page.locator("#gestureSetupContent, #captureInputContent").evaluateAll((contents) => (
15271530
contents.map((content) => ({
15281531
columnGap: getComputedStyle(content).columnGap,
@@ -1564,22 +1567,31 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15641567
const keyboardGestureButtons = gestureFlowLayout.groupLayouts.find((entry) => entry.label === "Keyboard").buttons;
15651568
expect(new Set(keyboardGestureButtons.map((entry) => entry.left)).size).toBeGreaterThan(1);
15661569
expect(new Set(keyboardGestureButtons.map((entry) => entry.top)).size).toBeGreaterThan(1);
1567-
await page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='wheel'] input").uncheck();
1570+
await page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='mouse'] input").uncheck();
15681571
await expect(page.locator(".input-mapping-v2__gesture-button", { hasText: "Wheel Up" })).toHaveCount(0);
1569-
await page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='wheel'] input").check();
1572+
await page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='mouse'] input").check();
15701573
await expect(page.locator(".input-mapping-v2__gesture-button", { hasText: "Wheel Up" })).toHaveCount(1);
15711574
const actionOptions = await page.locator("#inputMappingV2ActionSelect option").allTextContents();
15721575
expect(actionOptions).toEqual([...actionOptions].sort((left, right) => left.localeCompare(right)));
15731576
expect(actionOptions).toEqual(expect.arrayContaining(["Move Left", "Confirm", "Cancel", "Fire", "Thrust", "Rotate Left", "Rotate Right", "Pause", "Select", "Start"]));
15741577
await expect(page.locator("#inputMappingV2ResetActionsButton")).toHaveCount(0);
1575-
await expect(page.locator("#inputMappingV2ClearActionButton")).toHaveText("Delete");
1576-
await expect(page.locator("#actionSetupContent .input-mapping-v2__button-row button")).toHaveText(["Add", "Delete", "Delete"]);
1577-
await expect(page.locator("#inputMappingV2RumbleFeedbackCheckbox")).toBeVisible();
1578+
await expect(page.locator("#inputMappingV2ClearActionButton")).toHaveText("Delete All");
1579+
await expect(page.locator("#actionSetupContent .input-mapping-v2__button-row button")).toHaveText(["Add", "Delete"]);
1580+
await expect(page.locator("#mappingPanelContent #inputMappingV2ClearActionButton")).toBeVisible();
1581+
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='gameController'] #inputMappingV2RumbleFeedbackCheckbox")).toBeVisible();
15781582
await page.locator("#inputMappingV2RumbleFeedbackCheckbox").check();
15791583
await expect(page.locator("#statusLog")).toHaveValue(/WARN Gamepad rumble unavailable:/);
15801584
await expect(page.locator("#previewOutput")).toContainText("No inputs captured yet.");
15811585
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(0);
15821586
expect(await page.locator("#previewOutput").evaluate((node) => getComputedStyle(node).overflowY)).toBe("auto");
1587+
const capturedMappingsActionPlacement = await page.locator("#mappingPanelContent").evaluate((content) => ({
1588+
lastElementClass: content.lastElementChild?.className,
1589+
lastButtonText: content.lastElementChild?.querySelector("button")?.textContent
1590+
}));
1591+
expect(capturedMappingsActionPlacement).toEqual({
1592+
lastElementClass: "input-mapping-v2__mapping-actions",
1593+
lastButtonText: "Delete All"
1594+
});
15831595
const captureFlowLayout = await page.locator("#captureInputContent").evaluate((content) => {
15841596
const rectFor = (selector) => {
15851597
const box = content.querySelector(selector).getBoundingClientRect();
@@ -1760,12 +1772,17 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17601772
expect(Math.round(mappingTileBox.width)).toBe(225);
17611773
expect(Math.round(mappingTileBox.height)).toBe(225);
17621774
await expect(page.locator("#previewOutput")).toContainText("Move Left");
1763-
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyA");
1764-
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyD");
1765-
await expect(page.locator("#previewOutput")).toContainText("Mouse Middle Button");
1766-
await expect(page.locator("#previewOutput")).toContainText("Mouse Drag Release");
1775+
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyA Press");
1776+
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyD Press");
1777+
await expect(page.locator("#previewOutput")).toContainText("Mouse Middle Button Click");
1778+
await expect(page.locator("#previewOutput")).toContainText("Mouse Left Button Drag Release");
17671779
await expect(page.locator("#previewOutput")).not.toContainText("Mouse Drag Rectangle");
1768-
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Mouse Drag Release" })).toHaveAttribute("title", /Bounds: x 12, y 16, width 60, height 80/);
1780+
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Mouse Left Button" }).filter({ hasText: "Drag Release" })).toHaveAttribute("title", /Bounds: x 12, y 16, width 60, height 80/);
1781+
await expect(page.locator("#inputMappingV2CaptureKeyboardButton")).toHaveClass(/has-used-input/);
1782+
await expect(page.locator("#inputMappingV2CaptureMouseButton")).toHaveClass(/has-used-input/);
1783+
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Keyboard KeyA Press");
1784+
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Mouse Middle Button Click");
1785+
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Mouse Left Button Drag Release");
17691786
await expect(page.locator("#inspectorOutput")).toContainText('"action": "moveLeft"');
17701787
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyA"');
17711788
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyD"');
@@ -1789,7 +1806,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17891806
await page.keyboard.press("KeyB");
17901807
await expect(page.locator("#statusLog")).toHaveValue(/OK Keyboard KeyB mapped to Confirm\./);
17911808
await expect(page.locator("#captureInputContent")).not.toContainText("Keyboard KeyB mapped to Confirm.");
1792-
await page.locator("#inputMappingV2ClearActionButton").click();
1809+
await page.locator("#inputMappingV2DeleteActionButton").click();
17931810
await expect(page.locator(".input-mapping-v2__mapping-card", { hasText: "Confirm" })).toHaveCount(0);
17941811
await expect(page.locator(".input-mapping-v2__mapping-card", { hasText: "Move Left" })).toHaveCount(1);
17951812
await expect(page.locator("#inputMappingV2ActionSelect option[value='confirm']")).toHaveJSProperty("disabled", false);
@@ -1872,8 +1889,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18721889
});
18731890
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).not.toHaveClass(/is-capturing/);
18741891
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyD");
1875-
await expect(page.locator("#previewOutput")).toContainText("Game Controller X");
1892+
await expect(page.locator("#previewOutput")).toContainText("Game Controller X Button");
18761893
await expect(page.locator("#previewOutput")).not.toContainText("(Gamepad 1)");
1894+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).toHaveClass(/has-used-input/);
1895+
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Game Controller X Button");
18771896
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Game Controller" }).filter({ hasText: "X" })).toHaveAttribute("title", "Logitech RumblePad 2 USB\nSTANDARD GAMEPAD\nVendor: 046d Product: c218\nX");
18781897
await expect(page.locator("#inspectorOutput")).toContainText('"source": "gamepad"');
18791898
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "Pad1:Button2"');
@@ -1929,6 +1948,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
19291948
await page.locator("#toolCopyJsonButton").click();
19301949
expect(await page.evaluate(() => window.__inputMappingV2Clipboard)).toContain('"toolId": "input-mapping-v2"');
19311950
await expect(page.locator("#statusLog")).toHaveValue(/OK Mapping JSON copied\./);
1951+
await page.locator("#inputMappingV2ClearActionButton").click();
1952+
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(0);
1953+
await expect(page.locator("#previewOutput")).toContainText("No inputs captured yet.");
1954+
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted \d+ captured mappings\./);
19321955
expect(pageErrors).toEqual([]);
19331956
} finally {
19341957
await workspaceV2CoverageReporter.stop(page);

0 commit comments

Comments
 (0)