Skip to content

Commit c21bb23

Browse files
committed
Audit final Input Mapping V2 engine ownership - PR_26140_119-final-input-mapping-v2-ownership-audit
1 parent 95cb5da commit c21bb23

6 files changed

Lines changed: 139 additions & 10 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Input Mapping V2 Final Ownership Audit Report
2+
3+
PR: `PR_26140_119-final-input-mapping-v2-ownership-audit`
4+
5+
Playwright impacted: Yes. This PR verifies and lightly stabilizes Input Mapping V2 runtime capture, Action Mapping(s) rendering, and Workspace V2 manifest launch behavior.
6+
7+
## Summary
8+
9+
- Final ownership audit passed after small cleanup fixes.
10+
- Input Mapping V2 continues to consume engine input services for capture/combo/release/hold/double-click/drag/device compatibility behavior.
11+
- Export and Copy JSON both use the filtered payload and exclude empty actions.
12+
- Active `games/Asteroids/game.manifest.json` contains `tools.input-mapping-v2`, and Workspace Manager V2 references/discovers the tool through the manifest/tool registry path.
13+
- Import remains intentionally disabled with an actionable UI reason, because Input Mapping V2 imports through Workspace Manager manifest launch data.
14+
15+
## Cleanup Applied
16+
17+
- `src/engine/input/PointerDragState.js`: mouse drag state now ignores move events when the originally pressed mouse button is no longer held, preventing accidental drag completion from unrelated mouse movement.
18+
- `src/engine/input/InputCaptureSession.js`: invalid gamepad inputs keep strict validation but refresh the capture timeout so users can correct the input without the original timer expiring mid-flow.
19+
- `tools/input-mapping-v2/js/ToolStarterApp.js`: gamepad button release bindings are displayed as transient live highlights so release gestures are visible long enough to verify.
20+
- `tools/input-mapping-v2/js/controls/PreviewPanelControl.js`: Action Mapping(s) card elements are preserved across refreshes when the action set is unchanged, preventing transient card detach during visual-state refreshes.
21+
- `tools/input-mapping-v2/styles/inputMappingV2.css`: center-column non-fill accordions size to content so Action Mapping(s) keeps the remaining vertical space.
22+
23+
## Audit Results
24+
25+
- JSON export/copy empty action filtering: PASS. `InputMappingState.payload()` filters actions with `inputs.length > 0`; explicit browser validation produced `emptyExportActions: 0`, `mappedExportActions: 1`, and matching copied/exported JSON.
26+
- Active manifest wiring: PASS. `games/Asteroids/game.manifest.json` contains `tools.input-mapping-v2` with `toolId: "input-mapping-v2"` and `engineInputModel: "src/engine/input/InputMap"`.
27+
- Workspace Manager discovery/load path: PASS. `tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`, `tools/toolRegistry.js`, and `tests/playwright/tools/WorkspaceManagerV2.spec.mjs` reference `input-mapping-v2`.
28+
- Local-only lifecycle audit: PASS. Search for local-only capture/combo/gesture lifecycle patterns in `tools/input-mapping-v2/js` returned no matches.
29+
- Engine ownership audit: PASS. Reusable capture/combo/release/hold/double-click/drag/device compatibility behavior is under `src/engine/input/**`.
30+
- Import behavior: PASS. Import controls are disabled with the actionable reason that Input Mapping V2 imports through Workspace Manager `game.manifest` launch data.
31+
- Schemas and sample JSON: unchanged.
32+
33+
## Validation
34+
35+
- Targeted syntax validation: PASS.
36+
- Engine input module import validation: PASS.
37+
- Input Mapping V2 focused Playwright tests: PASS, `12 passed`.
38+
- Workspace V2 suite: PASS, `71 passed`.
39+
- Export/copy JSON empty-action confirmation: PASS.
40+
- Active manifest check: PASS, `tools.input-mapping-v2` present.
41+
- Workspace Manager load-path reference check: PASS.
42+
- `git diff --check`: PASS, no whitespace errors. Git reported the existing CRLF normalization warning for `tools/input-mapping-v2/styles/inputMappingV2.css`.
43+
44+
## Manual Validation
45+
46+
1. Launch Workspace Manager V2 and select the Asteroids manifest.
47+
2. Open Input Mapping V2 from the manifest-backed tool list.
48+
3. Add an Action Mapping(s) tile, capture keyboard/mouse/gamepad inputs, and confirm mappings render on the selected tile.
49+
4. Click JSON and Copy JSON; confirm only actions with inputs are present.
50+
5. Use Import; confirm it is disabled with the manifest-launch-data reason.
51+
52+
Expected outcome: Input Mapping V2 loads from manifest wiring, captures through engine-backed flows, highlights live used inputs, and exports filtered JSON with no empty actions.
53+
54+
## Out Of Scope
55+
56+
- Full samples smoke test was not run. This PR is scoped to Input Mapping V2 and Workspace V2 validation, and sample JSON remains explicitly out of scope.
57+
- No sample JSON was touched.

src/engine/input/InputCaptureSession.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ export default class InputCaptureSession {
249249
};
250250
}
251251
if (!result?.ok) {
252+
if (result?.invalid === true) {
253+
this.scheduleCaptureTimeout();
254+
}
252255
return {
253256
...warning(result?.message ?? 'Gamepad capture unavailable.'),
254257
cancel: result?.invalid !== true

src/engine/input/PointerDragState.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class PointerDragState {
5454
}
5555

5656
pointerMove(event = {}) {
57-
if (!this.isDown || !this.matchesActivePointer(event)) {
57+
if (!this.isDown || !this.matchesActivePointer(event) || !isButtonStillPressed(event, this.button)) {
5858
return;
5959
}
6060

@@ -178,6 +178,31 @@ function pointFromEvent(event) {
178178
};
179179
}
180180

181+
function isButtonStillPressed(event, button) {
182+
if (event.buttons === undefined || event.buttons === null) {
183+
return true;
184+
}
185+
const buttons = Number(event.buttons);
186+
if (!Number.isFinite(buttons)) {
187+
return true;
188+
}
189+
return (buttons & buttonMask(button)) !== 0;
190+
}
191+
192+
function buttonMask(button) {
193+
const normalizedButton = Number(button);
194+
if (normalizedButton === 1) {
195+
return 4;
196+
}
197+
if (normalizedButton === 2) {
198+
return 2;
199+
}
200+
if (normalizedButton > 2) {
201+
return 1 << normalizedButton;
202+
}
203+
return 1;
204+
}
205+
181206
function pointerIdFromEvent(event) {
182207
const pointerId = Number(event.pointerId);
183208
return Number.isFinite(pointerId) ? pointerId : null;

tools/input-mapping-v2/js/ToolStarterApp.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -631,10 +631,22 @@ export class ToolStarterApp {
631631
}
632632

633633
syncGamepadActiveInputBindings() {
634+
const activeBindings = this.engineInputSources.activeGamepadBindings();
635+
const releaseBindings = activeBindings.filter(isGamepadReleaseBinding);
636+
const continuousBindings = activeBindings.filter((binding) => !isGamepadReleaseBinding(binding));
637+
const releaseChanged = this.engineInputSources.activateInputBindings(releaseBindings, {
638+
durationMs: 360,
639+
transient: true
640+
});
641+
if (releaseChanged) {
642+
this.window.setTimeout?.(() => {
643+
this.refreshActions();
644+
}, 360);
645+
}
634646
return this.replaceActiveInputBindings(
635-
(binding) => binding.startsWith("Pad"),
636-
this.engineInputSources.activeGamepadBindings()
637-
);
647+
(binding) => binding.startsWith("Pad") && !isGamepadReleaseBinding(binding),
648+
continuousBindings
649+
) || releaseChanged;
638650
}
639651

640652
replaceActiveInputBindings(shouldReplace, nextBindings) {
@@ -985,6 +997,10 @@ function allCaptureAvailable() {
985997
};
986998
}
987999

1000+
function isGamepadReleaseBinding(binding) {
1001+
return String(binding || "").includes(":GameControllerButtonRelease");
1002+
}
1003+
9881004
function mouseCaptureMessage(gesture, actionLabel) {
9891005
if (gesture?.binding === "MouseDoubleClick") {
9901006
return `Double-click a mouse button to bind it to ${actionLabel}.`;

tools/input-mapping-v2/js/controls/PreviewPanelControl.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export class PreviewPanelControl {
2929
this.output.replaceChildren(this.createEmptyState());
3030
return;
3131
}
32+
const currentCards = this.actionCards();
33+
if (cardsMatchActions(currentCards, visibleActions)) {
34+
currentCards.forEach((card, index) => {
35+
this.updateActionCard(card, visibleActions[index], selectedActionId);
36+
});
37+
this.restoreSelectedCardScrollTop(selectedActionId, selectedScrollTop);
38+
return;
39+
}
3240
this.output.replaceChildren(...visibleActions.map((action) => this.createActionCard(action, selectedActionId)));
3341
this.restoreSelectedCardScrollTop(selectedActionId, selectedScrollTop);
3442
}
@@ -40,19 +48,26 @@ export class PreviewPanelControl {
4048
card.dataset.inputMappingTileActionId = action.id;
4149
card.tabIndex = 0;
4250
card.role = "button";
43-
card.ariaCurrent = isSelected ? "true" : "false";
44-
card.setAttribute("aria-label", `Select ${action.label} mapping`);
4551
card.addEventListener("click", () => {
46-
this.onSelectAction(action.id);
52+
this.onSelectAction(card.dataset.inputMappingTileActionId);
4753
});
4854
card.addEventListener("keydown", (event) => {
4955
if (event.key !== "Enter" && event.key !== " ") {
5056
return;
5157
}
5258
event.preventDefault();
53-
this.onSelectAction(action.id);
59+
this.onSelectAction(card.dataset.inputMappingTileActionId);
5460
});
5561

62+
return this.updateActionCard(card, action, selectedActionId);
63+
}
64+
65+
updateActionCard(card, action, selectedActionId) {
66+
const isSelected = action.id === selectedActionId;
67+
card.className = `input-mapping-v2__mapping-card${isSelected ? " is-selected" : ""}`;
68+
card.dataset.inputMappingTileActionId = action.id;
69+
card.ariaCurrent = isSelected ? "true" : "false";
70+
card.setAttribute("aria-label", `Select ${action.label} mapping`);
5671
const actionLabel = document.createElement("strong");
5772
actionLabel.className = "input-mapping-v2__tile-action-label";
5873
actionLabel.dataset.inputMappingTileActionId = action.id;
@@ -69,7 +84,7 @@ export class PreviewPanelControl {
6984
tokens.append(...this.createInputTokens(action, isSelected));
7085
}
7186

72-
card.append(actionLabel, tokens);
87+
card.replaceChildren(actionLabel, tokens);
7388
return card;
7489
}
7590

@@ -135,9 +150,18 @@ export class PreviewPanelControl {
135150
if (!actionId) {
136151
return null;
137152
}
138-
return Array.from(this.output.querySelectorAll(".input-mapping-v2__mapping-card"))
153+
return this.actionCards()
139154
.find((card) => card.dataset.inputMappingTileActionId === actionId) ?? null;
140155
}
156+
157+
actionCards() {
158+
return Array.from(this.output.querySelectorAll(".input-mapping-v2__mapping-card"));
159+
}
160+
}
161+
162+
function cardsMatchActions(cards, actions) {
163+
return cards.length === actions.length
164+
&& cards.every((card, index) => card.dataset.inputMappingTileActionId === actions[index].id);
141165
}
142166

143167
function inputLabelLines(input) {

tools/input-mapping-v2/styles/inputMappingV2.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@
266266
min-height: 300px;
267267
}
268268

269+
.input-mapping-v2 .tool-starter__panel--center > .tool-starter__accordion:not(.tool-starter__accordion--fill) {
270+
flex: 0 0 auto;
271+
}
272+
269273
.input-mapping-v2 .tool-starter__panel--right > .tool-starter__accordion.is-open {
270274
flex: 1 1 0;
271275
}

0 commit comments

Comments
 (0)