Skip to content

Commit 4fe9a64

Browse files
committed
Add Audio SFX undo redo editing history - PR_26145_024-audio-sfx-undo-redo
1 parent 0ed5299 commit 4fe9a64

8 files changed

Lines changed: 231 additions & 28 deletions

File tree

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
# PR_26145_023 Audio / SFX V8 Coverage
1+
# Audio / SFX Focused V8 Coverage
22

3-
Coverage source: focused Playwright/Chromium validation for Audio / SFX playback layout and Stop All behavior.
3+
Command: `node tmp/audio-sfx-undo-redo-focused.mjs`
4+
Source: Playwright Chromium built-in V8 coverage.
5+
Thresholds: none enforced; coverage is advisory.
46

5-
Changed runtime JavaScript files:
6-
7-
(100%) tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js - 18081/18081 bytes used in focused playback layout validation
8-
(100%) tools/audio-sfx-playground-v2/js/bootstrap.js - 4991/4991 bytes used in focused playback layout validation
9-
(100%) tools/audio-sfx-playground-v2/js/controls/ActionNavControl.js - 4048/4048 bytes used in focused playback layout validation
10-
(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - 27599/27599 bytes used in focused playback layout validation
11-
(100%) tools/audio-sfx-playground-v2/js/services/AudioSfxEngine.js - 6419/6419 bytes used in focused playback layout validation
12-
13-
Thresholds: none enforced. Missing changed runtime JS files would be reported as WARN.
7+
Changed runtime JS files:
8+
(100%) tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js - executed lines 642/642; advisory V8 coverage
9+
(100%) tools/audio-sfx-playground-v2/js/bootstrap.js - executed lines 100/100; advisory V8 coverage
10+
(100%) tools/audio-sfx-playground-v2/js/controls/ActionNavControl.js - executed lines 115/115; advisory V8 coverage
11+
(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - executed lines 704/704; advisory V8 coverage
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Audio / SFX Undo Redo Focused Playwright Validation
2+
3+
Command: `node tmp/audio-sfx-undo-redo-focused.mjs`
4+
5+
Result: PASS
6+
7+
Validated behavior:
8+
- PASS Undo and Redo are disabled with no available history.
9+
- PASS Add sound creates history and enables Undo only.
10+
- PASS Slider edit can undo and redo.
11+
- PASS Waveform change can undo and redo.
12+
- PASS Noise toggle and noise control changes can undo and redo.
13+
- PASS Looping checkbox change can undo and redo.
14+
- PASS Sound Style change can undo and redo.
15+
- PASS Rename can undo and redo.
16+
- PASS Delete sound can undo and redo.
17+
- PASS Valid Import JSON can undo and redo.
18+
- PASS Play, Stop, Stop All, Copy, Export, and tile select do not create history.
19+
- PASS Redo history clears after a new edit following Undo.
20+
- PASS Workspace dirty notifier receives undoable edit, undo, and redo updates.
21+
- PASS No console errors or page errors were observed.
22+
23+
Full samples smoke test: skipped because this PR only impacts Audio / SFX Playground V2 editing behavior.
24+
25+
## Workspace V2 Suite
26+
27+
Command: `PLAYWRIGHT_BROWSERS_PATH=0 npm.cmd run test:workspace-v2`
28+
29+
Result: FAIL, 66 passed / 6 failed. The full suite was run once and was not rerun. Failures are outside the Audio / SFX Playground V2 Undo/Redo behavior covered by this PR.
30+
31+
Exact failing tests:
32+
- `Workspace Manager V2 bootstrap > shows Object Vector Studio V2 layout shell and schema-only palette gate`
33+
- `Workspace Manager V2 bootstrap > compacts Object Vector Studio V2 geometry layouts and selected palette state`
34+
- `Workspace Manager V2 bootstrap > resolves game manifest schema refs from the game schema during repo discovery`
35+
- `Workspace Manager V2 bootstrap > enables object vector and collision tools only from manifest geometry without fallback defaults`
36+
- `Workspace Manager V2 bootstrap > uses header lifecycle controls and launches tools from fixed Workspace Manager V2 tiles`
37+
- `Workspace Manager V2 bootstrap > owns temporary UAT manifest seeding and launches Asset Manager V2 through session context`
38+
39+
Known unrelated failure notes:
40+
- Object Vector Studio V2 layout/palette assertions failed before any Audio / SFX interaction.
41+
- Repo discovery checks still expect `AI Target Dummy`; this PR explicitly does not depend on or modify `games/AITargetDummy/game.manifest.json`.
42+
- The fixed Workspace Manager tiles test failed on an existing Workspace schemaRole expectation for Audio / SFX launch metadata; this PR did not modify Workspace Manager hydration or Audio / SFX schema metadata.
43+
- The UAT launch test timed out in Workspace/Asset Manager coverage shutdown after prior suite failures.

tools/audio-sfx-playground-v2/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
101101
<span class="accordion-v2__icon" aria-hidden="true">+</span>
102102
</button>
103103
<div class="audio-sfx__accordion-actions audio-sfx__design-actions" aria-label="Sound Design playback actions">
104+
<button id="toolUndoButton" type="button">Undo</button>
105+
<button id="toolRedoButton" type="button">Redo</button>
104106
<button id="toolPlayButton" type="button">Play</button>
105107
<button id="toolStopButton" type="button">Stop</button>
106108
<button id="toolStopAllButton" type="button">Stop All</button>

tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js

Lines changed: 150 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ function normalizeSoundName(name) {
4343
return name.trim().toLowerCase();
4444
}
4545

46+
function cloneSoundEntries(soundEntries) {
47+
return soundEntries.map((entry) => ({
48+
id: entry.id,
49+
sound: cloneSound(entry.sound)
50+
}));
51+
}
52+
53+
function snapshotsMatch(left, right) {
54+
return JSON.stringify(left) === JSON.stringify(right);
55+
}
56+
4657
function activeSoundFromToolState(toolState) {
4758
return toolState.payload.sounds.find((entry) => entry.id === toolState.payload.activeSoundId)?.sound || null;
4859
}
@@ -90,6 +101,9 @@ export class AudioSfxPlaygroundV2App {
90101
this.soundEntries = [];
91102
this.statusLog = statusLog;
92103
this.tileList = tileList;
104+
this.redoStack = [];
105+
this.undoStack = [];
106+
this.historyBaselineSnapshot = null;
93107
this.window = windowRef;
94108
this.workspaceDirtyNotifier = workspaceDirtyNotifier;
95109
}
@@ -108,8 +122,14 @@ export class AudioSfxPlaygroundV2App {
108122
onToolPlay: () => {
109123
void this.play();
110124
},
125+
onToolRedo: () => {
126+
void this.redo();
127+
},
111128
onToolStopAll: () => this.stopAll(),
112129
onToolStop: () => this.stop(),
130+
onToolUndo: () => {
131+
void this.undo();
132+
},
113133
onWorkspaceCopyManifest: () => this.statusLog.write("Copy manifest action ready for workspace wiring."),
114134
onWorkspaceExportManifest: () => this.statusLog.write("Export manifest action ready for workspace wiring."),
115135
onWorkspaceImportManifest: () => this.statusLog.write("Import manifest action ready for workspace wiring.")
@@ -126,13 +146,15 @@ export class AudioSfxPlaygroundV2App {
126146
}
127147
});
128148
this.statusLog.mount();
149+
this.refreshHistoryActions();
129150
void this.finishStartup();
130151
}
131152

132153
async finishStartup() {
133154
await this.loadWorkspacePayload();
134155
this.renderSoundList();
135156
this.refreshPreview();
157+
this.resetHistoryBaseline();
136158
this.statusLog.write("Audio / SFX Playground V2 ready.");
137159
}
138160

@@ -157,10 +179,7 @@ export class AudioSfxPlaygroundV2App {
157179
this.statusLog.error(`Workspace payload load failed: ${validation.message}`);
158180
return;
159181
}
160-
const nextSoundEntries = validation.value.soundEntries.map((entry) => ({
161-
id: entry.id,
162-
sound: cloneSound(entry.sound)
163-
}));
182+
const nextSoundEntries = cloneSoundEntries(validation.value.soundEntries);
164183
this.activeSoundId = validation.value.activeSoundId;
165184
this.soundEntries = nextSoundEntries;
166185
this.nextSoundNumber = nextSoundNumberAfter(nextSoundEntries);
@@ -198,14 +217,113 @@ export class AudioSfxPlaygroundV2App {
198217
}
199218

200219
handleEditorChange() {
220+
const beforeSnapshot = this.historyBaselineSnapshot;
201221
const validation = this.controls.validate({ nameOverride: this.activeSoundName() });
202222
if (validation.valid && this.updateActiveSound(this.soundForActiveEditorValue(validation.value))) {
203223
this.renderSoundList();
204-
void this.syncWorkspaceDirty("audio-sfx-editor-change", ["data.sounds"]);
224+
this.commitUndoableChange({
225+
beforeSnapshot,
226+
changedKeys: ["data.sounds"],
227+
reason: "audio-sfx-editor-change"
228+
});
229+
}
230+
if (validation.valid && !this.activeSoundId) {
231+
this.commitUndoableChange({
232+
beforeSnapshot,
233+
changedKeys: ["data.sounds"],
234+
reason: "audio-sfx-editor-change",
235+
shouldSyncDirty: false
236+
});
205237
}
206238
this.refreshPreview();
207239
}
208240

241+
createHistorySnapshot(editorSound = null) {
242+
const validation = editorSound
243+
? { valid: true, value: editorSound }
244+
: this.controls.validate();
245+
if (!validation.valid) {
246+
return null;
247+
}
248+
return {
249+
activeSoundId: this.activeSoundId,
250+
editorSound: cloneSound(validation.value),
251+
editorStyleProfile: this.controls.currentStyleProfile(),
252+
nextSoundNumber: this.nextSoundNumber,
253+
soundEntries: cloneSoundEntries(this.soundEntries)
254+
};
255+
}
256+
257+
resetHistoryBaseline() {
258+
this.historyBaselineSnapshot = this.createHistorySnapshot();
259+
this.refreshHistoryActions();
260+
}
261+
262+
commitUndoableChange({ beforeSnapshot, changedKeys, reason, shouldSyncDirty = true }) {
263+
const afterSnapshot = this.createHistorySnapshot();
264+
if (!beforeSnapshot || !afterSnapshot || snapshotsMatch(beforeSnapshot, afterSnapshot)) {
265+
this.historyBaselineSnapshot = afterSnapshot;
266+
this.refreshHistoryActions();
267+
return false;
268+
}
269+
this.undoStack.push(beforeSnapshot);
270+
this.redoStack = [];
271+
this.historyBaselineSnapshot = afterSnapshot;
272+
this.refreshHistoryActions();
273+
if (shouldSyncDirty) {
274+
void this.syncWorkspaceDirty(reason, changedKeys);
275+
}
276+
return true;
277+
}
278+
279+
restoreHistorySnapshot(snapshot) {
280+
this.activeSoundId = snapshot.activeSoundId;
281+
this.soundEntries = cloneSoundEntries(snapshot.soundEntries);
282+
this.nextSoundNumber = snapshot.nextSoundNumber;
283+
this.controls.loadSound(snapshot.editorSound, snapshot.editorStyleProfile);
284+
this.renderSoundList();
285+
this.refreshPreview();
286+
this.historyBaselineSnapshot = this.createHistorySnapshot();
287+
this.refreshHistoryActions();
288+
}
289+
290+
refreshHistoryActions() {
291+
this.actionNav.setUndoRedoActionsEnabled({
292+
canRedo: this.redoStack.length > 0,
293+
canUndo: this.undoStack.length > 0
294+
});
295+
}
296+
297+
async undo() {
298+
if (!this.undoStack.length) {
299+
this.refreshHistoryActions();
300+
return;
301+
}
302+
const currentSnapshot = this.createHistorySnapshot();
303+
const previousSnapshot = this.undoStack.pop();
304+
if (currentSnapshot) {
305+
this.redoStack.push(currentSnapshot);
306+
}
307+
this.restoreHistorySnapshot(previousSnapshot);
308+
await this.syncWorkspaceDirty("audio-sfx-undo", ["data.sounds", "data.activeSoundId"]);
309+
this.statusLog.write("Undid last Audio / SFX edit.");
310+
}
311+
312+
async redo() {
313+
if (!this.redoStack.length) {
314+
this.refreshHistoryActions();
315+
return;
316+
}
317+
const currentSnapshot = this.createHistorySnapshot();
318+
const nextSnapshot = this.redoStack.pop();
319+
if (currentSnapshot) {
320+
this.undoStack.push(currentSnapshot);
321+
}
322+
this.restoreHistorySnapshot(nextSnapshot);
323+
await this.syncWorkspaceDirty("audio-sfx-redo", ["data.sounds", "data.activeSoundId"]);
324+
this.statusLog.write("Redid last Audio / SFX edit.");
325+
}
326+
209327
async syncWorkspaceDirty(reason, changedKeys) {
210328
if (typeof this.workspaceDirtyNotifier !== "function") {
211329
return;
@@ -296,10 +414,15 @@ export class AudioSfxPlaygroundV2App {
296414
return;
297415
}
298416

417+
const beforeSnapshot = this.createHistorySnapshot();
299418
const entry = this.createSoundEntry(validation.value);
300419
this.renderSoundList();
301420
this.refreshPreview();
302-
void this.syncWorkspaceDirty("audio-sfx-sound-added", ["data.sounds", "data.activeSoundId"]);
421+
this.commitUndoableChange({
422+
beforeSnapshot,
423+
changedKeys: ["data.sounds", "data.activeSoundId"],
424+
reason: "audio-sfx-sound-added"
425+
});
303426
this.statusLog.write(`Added ${entry.sound.name}.`);
304427
}
305428

@@ -321,11 +444,16 @@ export class AudioSfxPlaygroundV2App {
321444
this.controls.showMessage("Name must be unique.", true);
322445
return;
323446
}
447+
const beforeSnapshot = this.createHistorySnapshot(entry.sound);
324448
this.audioEngine.stop(entry.sound.name);
325449
entry.sound.name = nextName;
326450
this.renderSoundList();
327451
this.refreshPreview();
328-
void this.syncWorkspaceDirty("audio-sfx-sound-renamed", ["data.sounds"]);
452+
this.commitUndoableChange({
453+
beforeSnapshot,
454+
changedKeys: ["data.sounds"],
455+
reason: "audio-sfx-sound-renamed"
456+
});
329457
this.statusLog.write(`Renamed SFX to ${entry.sound.name}.`);
330458
}
331459

@@ -339,6 +467,7 @@ export class AudioSfxPlaygroundV2App {
339467
this.controls.loadSound(entry.sound);
340468
this.renderSoundList();
341469
this.refreshPreview();
470+
this.resetHistoryBaseline();
342471
this.statusLog.write(`Loaded ${entry.sound.name}.`);
343472
try {
344473
const playbackResult = await this.audioEngine.play(entry.sound);
@@ -420,6 +549,7 @@ export class AudioSfxPlaygroundV2App {
420549
this.controls.setDeleteEnabled(false);
421550
return;
422551
}
552+
const beforeSnapshot = this.createHistorySnapshot();
423553
const [entry] = this.soundEntries.splice(entryIndex, 1);
424554
this.audioEngine.stop(entry.sound.name);
425555
const nextEntry = this.soundEntries[Math.min(entryIndex, this.soundEntries.length - 1)] || null;
@@ -429,7 +559,11 @@ export class AudioSfxPlaygroundV2App {
429559
}
430560
this.renderSoundList();
431561
this.refreshPreview();
432-
void this.syncWorkspaceDirty("audio-sfx-sound-deleted", ["data.sounds", "data.activeSoundId"]);
562+
this.commitUndoableChange({
563+
beforeSnapshot,
564+
changedKeys: ["data.sounds", "data.activeSoundId"],
565+
reason: "audio-sfx-sound-deleted"
566+
});
433567
this.statusLog.write(`Deleted ${entry.sound.name}.`);
434568
}
435569

@@ -443,6 +577,7 @@ export class AudioSfxPlaygroundV2App {
443577
return { valid: false, message: activeSoundEntry.message, toolState: null, json: "" };
444578
}
445579
this.renderSoundList();
580+
this.resetHistoryBaseline();
446581
const toolState = this.serializer.createToolState({
447582
activeSoundId: this.activeSoundId,
448583
soundEntries: this.soundEntries
@@ -486,17 +621,19 @@ export class AudioSfxPlaygroundV2App {
486621
this.statusLog.error(`Import JSON failed: ${result.message}`);
487622
return;
488623
}
489-
const nextSoundEntries = result.value.soundEntries.map((entry) => ({
490-
id: entry.id,
491-
sound: cloneSound(entry.sound)
492-
}));
624+
const beforeSnapshot = this.createHistorySnapshot();
625+
const nextSoundEntries = cloneSoundEntries(result.value.soundEntries);
493626
this.activeSoundId = result.value.activeSoundId;
494627
this.soundEntries = nextSoundEntries;
495628
this.nextSoundNumber = nextSoundNumberAfter(nextSoundEntries);
496629
this.controls.loadSound(result.value.sound);
497630
this.renderSoundList();
498631
this.refreshPreview();
499-
await this.syncWorkspaceDirty("audio-sfx-json-imported", ["data.sounds", "data.activeSoundId"]);
632+
this.commitUndoableChange({
633+
beforeSnapshot,
634+
changedKeys: ["data.sounds", "data.activeSoundId"],
635+
reason: "audio-sfx-json-imported"
636+
});
500637
this.statusLog.write(`Imported JSON from ${file.name}.`);
501638
} catch (error) {
502639
this.statusLog.error(`Import JSON failed: ${error.message}`);

tools/audio-sfx-playground-v2/js/bootstrap.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ window.addEventListener("DOMContentLoaded", () => {
3939
toolImportJsonInput: requireElement("#toolImportJsonInput"),
4040
toolNav: requireElement(".tool-starter__tool__menu"),
4141
toolPlayButton: requireElement("#toolPlayButton"),
42+
toolRedoButton: requireElement("#toolRedoButton"),
4243
toolStopAllButton: requireElement("#toolStopAllButton"),
4344
toolStopButton: requireElement("#toolStopButton"),
45+
toolUndoButton: requireElement("#toolUndoButton"),
4446
workspaceCopyManifestButton: requireElement("#workspaceCopyManifestButton"),
4547
workspaceExportManifestButton: requireElement("#workspaceExportManifestButton"),
4648
workspaceImportManifestButton: requireElement("#workspaceImportManifestButton"),

0 commit comments

Comments
 (0)