Skip to content

Commit cf16bef

Browse files
Code2Collapseclaude
andcommitted
fix(WanDirector): Delete removes the selected clip, not the whole node (port of feat bb4a851)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 63983a9 commit cf16bef

1 file changed

Lines changed: 39 additions & 3 deletions

File tree

js/wan_director_timeline.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -683,10 +683,36 @@ class TimelineEditor {
683683
}
684684
}, { passive: false });
685685

686+
// Track pointer presence so a focus-independent Delete handler knows the
687+
// user is working inside the timeline (not the graph).
688+
this._pointerInside = false;
689+
this.container.addEventListener("mouseenter", () => { this._pointerInside = true; });
690+
this.container.addEventListener("mouseleave", () => { this._pointerInside = false; });
691+
692+
// THE FIX for "Delete wipes the whole node": ComfyUI has a global
693+
// keydown that deletes the selected node. When a clip/lane is selected in
694+
// the timeline, Delete must remove THAT item and never reach ComfyUI. A
695+
// window capture-phase listener runs BEFORE ComfyUI's, and when the
696+
// pointer is inside the widget (or focus is within it) with a selection,
697+
// it deletes the segment and stops the event dead.
698+
this._onWinKeyDown = (e) => {
699+
if (e.key !== "Delete" && e.key !== "Backspace") return;
700+
const focusInside = this.container.contains(document.activeElement);
701+
const editing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || "")
702+
&& this.container.contains(document.activeElement);
703+
if (editing) return; // let text fields handle it
704+
if (!(this._pointerInside || focusInside)) return;
705+
if (!this.selection || this.selection.idx < 0 || !this.selection.type) return;
706+
this.deleteSelected();
707+
e.preventDefault();
708+
e.stopImmediatePropagation(); // ComfyUI never sees it → node survives
709+
};
710+
window.addEventListener("keydown", this._onWinKeyDown, true);
711+
686712
// Keyboard (canvas focused)
687713
this.cvs.addEventListener("keydown", (e) => {
688714
const sel = this.selection;
689-
if (e.key === "Delete" || e.key === "Backspace") { this.deleteSelected(); e.preventDefault(); }
715+
if (e.key === "Delete" || e.key === "Backspace") { this.deleteSelected(); e.preventDefault(); e.stopPropagation(); }
690716
else if ((e.key === "c" || e.key === "C") && (e.ctrlKey || e.metaKey)) {
691717
if (sel.type && sel.idx >= 0) this._copySegment({ segType: sel.type, idx: sel.idx });
692718
e.preventDefault();
@@ -1068,13 +1094,13 @@ class TimelineEditor {
10681094
(seg._thumbs || []).forEach((im) => { im.onload = () => this.render(); });
10691095
// LTX-style: a SCENE video that carries an audio track also drops a
10701096
// waveform on the AUDIO track (control videos don't). Silent if no audio.
1071-
if (!toControl) { try { await this._autoExtractVideoAudio(file, filename); } catch (_) {} }
1097+
if (!toControl) { try { await this._autoExtractVideoAudio(file, filename, seg.id); } catch (_) {} }
10721098
}
10731099

10741100
/** Decode a video's embedded audio and add it as a waveform on the AUDIO
10751101
* track, referencing the same already-uploaded file (no re-upload). Skips
10761102
* silently when the video has no audio track. Mirrors LTX Director. */
1077-
async _autoExtractVideoAudio(file, videoFilename) {
1103+
async _autoExtractVideoAudio(file, videoFilename, sourceVideoId) {
10781104
// If an audio clip for this exact file already exists, don't duplicate.
10791105
if (this.audioSegments.some(a => a.audioFile === videoFilename)) return;
10801106
let peaksInfo = null;
@@ -1088,6 +1114,9 @@ class TimelineEditor {
10881114
audioDurationFrames: peaksInfo.durFrames,
10891115
audioFile: videoFilename, fileName: file.name,
10901116
waveformPeaks: peaksInfo.peaks, fromVideo: true,
1117+
// Linked to its parent SCENE video: deleting the video deletes this
1118+
// audio too (they're the same source), like LTX Director.
1119+
sourceVideoId: sourceVideoId || null,
10911120
};
10921121
this.audioBuffers.set(videoFilename, peaksInfo.audio);
10931122
this.audioSegments.push(seg);
@@ -1126,7 +1155,13 @@ class TimelineEditor {
11261155
if (idx < 0) return;
11271156
const arr = this._segArr(type);
11281157
if (idx < arr.length) {
1158+
const removed = arr[idx];
11291159
arr.splice(idx, 1);
1160+
// Deleting a SCENE video also removes its linked auto-extracted audio
1161+
// (same source file), like LTX Director — video + its audio are one.
1162+
if (removed && removed.type === "video" && removed.id) {
1163+
this.audioSegments = this.audioSegments.filter(a => a.sourceVideoId !== removed.id);
1164+
}
11301165
this.selection = { type: null, idx: -1 };
11311166
this.commitChanges();
11321167
if (this._isV2(type)) this._updateV2Props(); else this._updatePropsPanel();
@@ -2320,6 +2355,7 @@ class TimelineEditor {
23202355
destroy() {
23212356
this._stopAudio();
23222357
this._dismissAddMenu();
2358+
try { if (this._onWinKeyDown) window.removeEventListener("keydown", this._onWinKeyDown, true); } catch {}
23232359
try { this._resizeObs?.disconnect(); } catch {}
23242360
if (this.audioCtx) try { this.audioCtx.close(); } catch {}
23252361
this.audioCtx = null;

0 commit comments

Comments
 (0)