diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index d08966a..22cf729 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9134,10 +9134,37 @@ const App = () => { const applyTrackState = (trackId, state) => { updateActiveTracks(prev => prev.map(t => { if (t.id !== trackId) return t; - return { - ...t, - ...state - }; + var updated = { ...t, ...state }; + if (state.sections) { + updated.sections = t.sections ? t.sections.map(function(s) { + var match = state.sections.find(function(ss) { return ss.id === s.id; }); + return match ? { ...s, start: match.start, duration: match.duration } : s; + }) : state.sections; + state.sections.forEach(function(ss) { + if (!updated.sections.find(function(s) { return s.id === ss.id; })) { + updated.sections.push(ss); + } + }); + } + if (state.midiItems) { + updated.midiItems = t.midiItems ? t.midiItems.map(function(m) { + var match = state.midiItems.find(function(sm) { return sm.id === m.id; }); + return match ? { ...m, startTime: match.startTime, duration: match.duration } : m; + }) : state.midiItems; + state.midiItems.forEach(function(sm) { + if (!updated.midiItems.find(function(m) { return m.id === sm.id; })) { + updated.midiItems.push(sm); + } + }); + } + if (state.clips) { + updated.clips = t.clips ? t.clips.map(function(c) { + var cid = c.id === 'default' ? 'default_' + t.id : c.id; + var match = state.clips.find(function(sc) { return sc.id === cid; }); + return match ? { ...c, startTime: match.startTime } : c; + }) : state.clips; + } + return updated; })); }; const getSelectedMidiItemInfo = () => { @@ -9166,7 +9193,9 @@ const App = () => { buffer: c.buffer, startTime: c.startTime, name: c.name - })) : null + })) : null, + sections: track.sections ? track.sections.map(function(s) { return { id: s.id, start: s.start, duration: s.duration, name: s.name, color: s.color, notes: s.notes ? s.notes.map(function(n) { return { id: n.id, pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity }; }) : null, tracks: s.tracks ? s.tracks.map(function(st) { return { id: st.id, name: st.name, color: st.color, clips: st.clips ? st.clips.map(function(c) { return { id: c.id, startTime: c.startTime, name: c.name, speed: c.speed, buffer: c.buffer }; }) : null, midiItems: st.midiItems ? st.midiItems.map(function(m) { return { id: m.id, startTime: m.startTime, duration: m.duration, name: m.name, notes: m.notes ? m.notes.map(function(n) { return { id: n.id, pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity }; }) : null }; }) : null }; }) : null }; }) : null, + midiItems: track.midiItems ? track.midiItems.map(function(m) { return { id: m.id, startTime: m.startTime, duration: m.duration, name: m.name, notes: m.notes ? m.notes.map(function(n) { return { id: n.id, pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity }; }) : null }; }) : null }; }; @@ -14076,6 +14105,7 @@ const App = () => { if (!track) return; if (multiIds) { var newOriginals = {}; + var dupBeforeSnap = captureTrackSnapshot(trackId); updateActiveTracks(function(prev) { return prev.map(function(t) { var updatedSections = t.sections ? t.sections.slice() : []; @@ -14111,13 +14141,14 @@ const App = () => { }); var newItemId = Object.keys(newOriginals)[0] || itemId; var origPos = newOriginals[newItemId] || { start: 0 }; - setDraggedSectionItem({ trackId, itemType, itemId: newItemId, clickOffset, multiIds: newOriginals, originalPositions: { [newItemId]: origPos.start } }); + setDraggedSectionItem({ trackId, itemType, itemId: newItemId, clickOffset, multiIds: newOriginals, originalPositions: { [newItemId]: origPos.start }, beforeSnap: dupBeforeSnap }); } else { var items = itemType === 'section' ? (track.sections || []) : (track.midiItems || []); var item = items.find(function(it) { return it.id === itemId; }); if (!item) return; var rearrangeNewId = itemType + '_dup_' + Date.now(); var newItem = { ...item, id: rearrangeNewId, name: item.name + ' (Copy)' }; + var singleDupBeforeSnap = captureTrackSnapshot(trackId); updateActiveTracks(function(prev) { return prev.map(function(t) { if (t.id !== trackId) return t; @@ -14125,14 +14156,15 @@ const App = () => { return itemType === 'section' ? { ...t, sections: updated } : { ...t, midiItems: updated }; }); }); - setDraggedSectionItem({ trackId, itemType, itemId: rearrangeNewId, clickOffset, isDuplicate: false, originalPositions: { [rearrangeNewId]: itemType === 'section' ? item.start : item.startTime } }); + setDraggedSectionItem({ trackId, itemType, itemId: rearrangeNewId, clickOffset, isDuplicate: false, originalPositions: { [rearrangeNewId]: itemType === 'section' ? item.start : item.startTime }, beforeSnap: singleDupBeforeSnap }); } } else { var curTrk = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === trackId) : null; var its = itemType === 'section' ? (curTrk?.sections || []) : (curTrk?.midiItems || []); var it = its.find(function(x) { return x.id === itemId; }); var origPos = it ? (itemType === 'section' ? it.start : it.startTime) : 0; - setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, originalPositions: { [itemId]: origPos } }); + var beforeSnap = captureTrackSnapshot(trackId); + setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, originalPositions: { [itemId]: origPos }, beforeSnap }); } }; handleSectionItemDragStartRef.current = handleSectionItemDragStart; @@ -14288,7 +14320,7 @@ const App = () => { if (!drag) return; var changed = false; var trackId = drag.trackId; - var beforeSnap = captureTrackSnapshot(trackId); + var beforeSnap = drag.beforeSnap; var origs = drag.originalPositions || {}; updateActiveTracks(function(prev) { return prev.map(function(t) { @@ -14308,8 +14340,6 @@ const App = () => { if (Math.abs(curStart - origStart) > 0.001) { hasChange = true; changed = true; - if (sec) item.start = origStart; - if (mid) item.startTime = origStart; } } } @@ -14318,9 +14348,9 @@ const App = () => { return { ...t, sections: updatedSections, midiItems: updatedMidi }; }); }); - if (changed) { + if (changed || drag.beforeSnap) { var afterSnap = captureTrackSnapshot(trackId); - pushAction('MOVE_ITEM', trackId, beforeSnap, afterSnap); + pushAction('MOVE_ITEM', trackId, drag.beforeSnap || beforeSnap, afterSnap); } if (drag.itemType === 'midiItem') { let finalTrackId = null; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 2cbdf48..7e03e52 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -263,7 +263,7 @@ const[editNameInput,setEditNameInput]=useState('');// ── Context Menu & Clip const[contextMenu,setContextMenu]=useState(null);// { x, y, trackId } const clipboardRef=useRef(null);// { buffer, name, volume, color } for copy/paste // ── Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension) ── -const[undoStack,setUndoStack]=useState([]);const[redoStack,setRedoStack]=useState([]);const MAX_UNDO=30;const pushAction=(actionType,trackId,beforeState,afterState)=>{const node={action_type:actionType,track_id:trackId,timestamp:Date.now(),before_state:beforeState,after_state:afterState};setUndoStack(prev=>{const next=[...prev,node];if(next.length>MAX_UNDO)next.shift();return next;});setRedoStack([]);};const handleUndo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canUndo()){const entry=window.UndoRedoEngine.undo();if(entry){if(entry.undo&&typeof entry.undo==='function')entry.undo(entry);showToast(`Undo: ${entry.label||entry.type}`,'info');return;}}if(undoStack.length===0)return;const last=undoStack[undoStack.length-1];setUndoStack(prev=>prev.slice(0,-1));setRedoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.before_state);showToast(`Undo: ${last.action_type}`,'info');};const handleRedo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canRedo()){const entry=window.UndoRedoEngine.redo();if(entry){if(entry.redo&&typeof entry.redo==='function')entry.redo(entry);showToast(`Redo: ${entry.label||entry.type}`,'info');return;}}if(redoStack.length===0)return;const last=redoStack[redoStack.length-1];setRedoStack(prev=>prev.slice(0,-1));setUndoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.after_state);showToast(`Redo: ${last.action_type}`,'info');};const applyTrackState=(trackId,state)=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,...state};}));};const getSelectedMidiItemInfo=()=>{if(!selectedItemIds||selectedItemIds.size!==1)return null;const selId=selectedItemIds.values().next().value;const tlist=activeTracks||tracks||[];for(const t of tlist){const found=(t.midiItems||[]).find(m=>m.id===selId);if(found)return{itemName:found.name,trackName:t.name,trackId:t.id,itemId:selId};}return null;};const captureTrackSnapshot=trackId=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return null;return{volumeDb:track.volumeDb,pan:track.pan,muted:track.muted,name:track.name,markers:JSON.parse(JSON.stringify(track.markers||[])),buffer:track.buffer,startTime:track.startTime||0,clips:track.clips?track.clips.map(c=>({id:c.id,buffer:c.buffer,startTime:c.startTime,name:c.name})):null};};const setBpmWithUndo=newBpm=>{const oldBpm=bpmRef.current;if(String(oldBpm)===String(newBpm))return;const entry={type:'SET_BPM',scope:'global',label:`BPM ${oldBpm} → ${newBpm}`,before:oldBpm,after:newBpm,undo:e=>{setBpm(e.before);showToast(`Undo: BPM → ${e.before}`,'info');},redo:e=>{setBpm(e.after);showToast(`Redo: BPM → ${e.after}`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setBpm(String(newBpm));};const setPlayheadWithUndo=newTime=>{const oldTime=currentTime;if(Math.abs(oldTime-newTime)<0.001)return;const entry={type:'SET_PLAYHEAD',scope:'global',label:`Playhead ${formatTimeSimple(oldTime)} → ${formatTimeSimple(newTime)}`,before:oldTime,after:newTime,undo:e=>{applyPlayheadDirect(e.before);showToast(`Undo: Playhead`,'info');},redo:e=>{applyPlayheadDirect(e.after);showToast(`Redo: Playhead`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);applyPlayheadDirect(newTime);};const applyPlayheadDirect=time=>{localSelectionAnchorRef.current=time;if(isPlaying){setCurrentTime(time);stopAllPlayback();setTimeout(()=>{startOffsetTimeRef.current=time;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(time);setIsPlaying(true);},50);}else{setCurrentTime(time);}};const setSelectionWithUndo=(newStart,newEnd,mode)=>{const oldStart=selectionRef.current.start;const oldEnd=selectionRef.current.end;const oldMode=selectionMode;if(oldStart===newStart&&oldEnd===newEnd&&oldMode===mode)return;const entry={type:'SET_SELECTION',scope:'global',label:`Selection`,before:{start:oldStart,end:oldEnd,mode:oldMode},after:{start:newStart,end:newEnd,mode:mode},undo:e=>{setSelectionStart(e.before.start);setSelectionEnd(e.before.end);setSelectionMode(e.before.mode);showToast(`Undo: Selection`,'info');},redo:e=>{setSelectionStart(e.after.start);setSelectionEnd(e.after.end);setSelectionMode(e.after.mode);showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectionStart(newStart);setSelectionEnd(newEnd);if(mode!==undefined)setSelectionMode(mode);};const setSelectedItemsWithUndo=newSet=>{const oldSet=selectedItemIdsRef.current;if(oldSet&&newSet&&oldSet.size===newSet.size&&[...oldSet].every(x=>newSet.has(x)))return;const entry={type:'SELECT_ITEMS',scope:'global',label:`Selection`,before:[...oldSet],after:[...newSet],undo:e=>{setSelectedItemIds(new Set(e.before));showToast(`Undo: Selection`,'info');},redo:e=>{setSelectedItemIds(new Set(e.after));showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectedItemIds(newSet);};const setAiPromptWithUndo=newText=>{const oldText=aiPrompt;if(oldText===newText)return;const entry={type:'SET_AI_PROMPT',scope:'global',label:`AI Prompt`,before:oldText,after:newText,undo:e=>{setAiPrompt(e.before);showToast(`Undo: AI Prompt`,'info');},redo:e=>{setAiPrompt(e.after);showToast(`Redo: AI Prompt`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setAiPrompt(newText);};const setTrackInstrumentWithUndo=(trackId,instrumentId,displayName,bankNumber,programNumber)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const oldInstrumentId=track.instrumentId;const oldInstrumentName=track.instrumentName;if(oldInstrumentId===instrumentId&&oldInstrumentName===displayName)return;const entry={type:'SET_INSTRUMENT',scope:'track:'+trackId,label:`Instrument ${track.name}`,before:{instrumentId:oldInstrumentId,instrumentName:oldInstrumentName,bankNumber:track.soundfont_bank,programNumber:track.instrumentProgram},after:{instrumentId,instrumentName:displayName,bankNumber,programNumber},undo:e=>{setTrackInstrumentWithProgram(trackId,e.before.instrumentId,e.before.programNumber,e.before.instrumentName,e.before.bankNumber);showToast(`Undo: Instrument`,'info');},redo:e=>{setTrackInstrumentWithProgram(trackId,e.after.instrumentId,e.after.programNumber,e.after.instrumentName,e.after.bankNumber);showToast(`Redo: Instrument`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setTrackInstrumentWithProgram(trackId,instrumentId,programNumber,displayName,bankNumber);};const createSectionWithUndo=(trackId,section)=>{const entry={type:'CREATE_SECTION',scope:'track:'+trackId,label:`Create Section`,before:null,after:section,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,sections:(t.sections||[]).filter(s=>s.id!==e.after.id)}:t));showToast(`Undo: Create Section`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,sections:[...(t.sections||[]),e.after]}:t));showToast(`Redo: Create Section`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const createMidiWithUndo=(trackId,midiItem)=>{const entry={type:'CREATE_MIDI',scope:'track:'+trackId,label:`Create MIDI Item`,before:null,after:midiItem,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==e.after.id)}:t));showToast(`Undo: Create MIDI`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:[...(t.midiItems||[]),e.after]}:t));showToast(`Redo: Create MIDI`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const createClipWithUndo=(trackId,clip)=>{const entry={type:'CREATE_CLIP',scope:'track:'+trackId,label:`Create Audio Clip`,before:null,after:clip,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,clips:(t.clips||[]).filter(c=>c.id!==e.after.id),buffer:(t.clips||[]).filter(c=>c.id!==e.after.id)[0]?.buffer||null,startTime:(t.clips||[]).filter(c=>c.id!==e.after.id)[0]?.startTime||0,name:(t.clips||[]).filter(c=>c.id!==e.after.id)[0]?.name||t.name}:t));showToast(`Undo: Create Clip`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,clips:[...(t.clips||[]),e.after]}:t));showToast(`Redo: Create Clip`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const deleteTrackWithUndo=(trackId,trackData)=>{const entry={type:'DELETE_TRACK',scope:'global',label:`Delete Track`,before:trackData,after:null,undo:e=>{if(e.before){setTracks(prev=>[...prev,e.before]);showToast(`Undo: Delete Track`,'info');}},redo:e=>{setTracks(prev=>prev.filter(t=>t.id!==trackId));showToast(`Redo: Delete Track`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};// ── Tab System (LOOP_EDITOR_2.md §1) ── +const[undoStack,setUndoStack]=useState([]);const[redoStack,setRedoStack]=useState([]);const MAX_UNDO=30;const pushAction=(actionType,trackId,beforeState,afterState)=>{const node={action_type:actionType,track_id:trackId,timestamp:Date.now(),before_state:beforeState,after_state:afterState};setUndoStack(prev=>{const next=[...prev,node];if(next.length>MAX_UNDO)next.shift();return next;});setRedoStack([]);};const handleUndo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canUndo()){const entry=window.UndoRedoEngine.undo();if(entry){if(entry.undo&&typeof entry.undo==='function')entry.undo(entry);showToast(`Undo: ${entry.label||entry.type}`,'info');return;}}if(undoStack.length===0)return;const last=undoStack[undoStack.length-1];setUndoStack(prev=>prev.slice(0,-1));setRedoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.before_state);showToast(`Undo: ${last.action_type}`,'info');};const handleRedo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canRedo()){const entry=window.UndoRedoEngine.redo();if(entry){if(entry.redo&&typeof entry.redo==='function')entry.redo(entry);showToast(`Redo: ${entry.label||entry.type}`,'info');return;}}if(redoStack.length===0)return;const last=redoStack[redoStack.length-1];setRedoStack(prev=>prev.slice(0,-1));setUndoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.after_state);showToast(`Redo: ${last.action_type}`,'info');};const applyTrackState=(trackId,state)=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;var updated={...t,...state};if(state.sections){updated.sections=t.sections?t.sections.map(function(s){var match=state.sections.find(function(ss){return ss.id===s.id;});return match?{...s,start:match.start,duration:match.duration}:s;}):state.sections;state.sections.forEach(function(ss){if(!updated.sections.find(function(s){return s.id===ss.id;})){updated.sections.push(ss);}});}if(state.midiItems){updated.midiItems=t.midiItems?t.midiItems.map(function(m){var match=state.midiItems.find(function(sm){return sm.id===m.id;});return match?{...m,startTime:match.startTime,duration:match.duration}:m;}):state.midiItems;state.midiItems.forEach(function(sm){if(!updated.midiItems.find(function(m){return m.id===sm.id;})){updated.midiItems.push(sm);}});}if(state.clips){updated.clips=t.clips?t.clips.map(function(c){var cid=c.id==='default'?'default_'+t.id:c.id;var match=state.clips.find(function(sc){return sc.id===cid;});return match?{...c,startTime:match.startTime}:c;}):state.clips;}return updated;}));};const getSelectedMidiItemInfo=()=>{if(!selectedItemIds||selectedItemIds.size!==1)return null;const selId=selectedItemIds.values().next().value;const tlist=activeTracks||tracks||[];for(const t of tlist){const found=(t.midiItems||[]).find(m=>m.id===selId);if(found)return{itemName:found.name,trackName:t.name,trackId:t.id,itemId:selId};}return null;};const captureTrackSnapshot=trackId=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return null;return{volumeDb:track.volumeDb,pan:track.pan,muted:track.muted,name:track.name,markers:JSON.parse(JSON.stringify(track.markers||[])),buffer:track.buffer,startTime:track.startTime||0,clips:track.clips?track.clips.map(c=>({id:c.id,buffer:c.buffer,startTime:c.startTime,name:c.name})):null,sections:track.sections?track.sections.map(function(s){return{id:s.id,start:s.start,duration:s.duration,name:s.name,color:s.color,notes:s.notes?s.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null,tracks:s.tracks?s.tracks.map(function(st){return{id:st.id,name:st.name,color:st.color,clips:st.clips?st.clips.map(function(c){return{id:c.id,startTime:c.startTime,name:c.name,speed:c.speed,buffer:c.buffer};}):null,midiItems:st.midiItems?st.midiItems.map(function(m){return{id:m.id,startTime:m.startTime,duration:m.duration,name:m.name,notes:m.notes?m.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null};}):null};}):null};}):null,midiItems:track.midiItems?track.midiItems.map(function(m){return{id:m.id,startTime:m.startTime,duration:m.duration,name:m.name,notes:m.notes?m.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null};}):null};};const setBpmWithUndo=newBpm=>{const oldBpm=bpmRef.current;if(String(oldBpm)===String(newBpm))return;const entry={type:'SET_BPM',scope:'global',label:`BPM ${oldBpm} → ${newBpm}`,before:oldBpm,after:newBpm,undo:e=>{setBpm(e.before);showToast(`Undo: BPM → ${e.before}`,'info');},redo:e=>{setBpm(e.after);showToast(`Redo: BPM → ${e.after}`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setBpm(String(newBpm));};const setPlayheadWithUndo=newTime=>{const oldTime=currentTime;if(Math.abs(oldTime-newTime)<0.001)return;const entry={type:'SET_PLAYHEAD',scope:'global',label:`Playhead ${formatTimeSimple(oldTime)} → ${formatTimeSimple(newTime)}`,before:oldTime,after:newTime,undo:e=>{applyPlayheadDirect(e.before);showToast(`Undo: Playhead`,'info');},redo:e=>{applyPlayheadDirect(e.after);showToast(`Redo: Playhead`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);applyPlayheadDirect(newTime);};const applyPlayheadDirect=time=>{localSelectionAnchorRef.current=time;if(isPlaying){setCurrentTime(time);stopAllPlayback();setTimeout(()=>{startOffsetTimeRef.current=time;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(time);setIsPlaying(true);},50);}else{setCurrentTime(time);}};const setSelectionWithUndo=(newStart,newEnd,mode)=>{const oldStart=selectionRef.current.start;const oldEnd=selectionRef.current.end;const oldMode=selectionMode;if(oldStart===newStart&&oldEnd===newEnd&&oldMode===mode)return;const entry={type:'SET_SELECTION',scope:'global',label:`Selection`,before:{start:oldStart,end:oldEnd,mode:oldMode},after:{start:newStart,end:newEnd,mode:mode},undo:e=>{setSelectionStart(e.before.start);setSelectionEnd(e.before.end);setSelectionMode(e.before.mode);showToast(`Undo: Selection`,'info');},redo:e=>{setSelectionStart(e.after.start);setSelectionEnd(e.after.end);setSelectionMode(e.after.mode);showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectionStart(newStart);setSelectionEnd(newEnd);if(mode!==undefined)setSelectionMode(mode);};const setSelectedItemsWithUndo=newSet=>{const oldSet=selectedItemIdsRef.current;if(oldSet&&newSet&&oldSet.size===newSet.size&&[...oldSet].every(x=>newSet.has(x)))return;const entry={type:'SELECT_ITEMS',scope:'global',label:`Selection`,before:[...oldSet],after:[...newSet],undo:e=>{setSelectedItemIds(new Set(e.before));showToast(`Undo: Selection`,'info');},redo:e=>{setSelectedItemIds(new Set(e.after));showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectedItemIds(newSet);};const setAiPromptWithUndo=newText=>{const oldText=aiPrompt;if(oldText===newText)return;const entry={type:'SET_AI_PROMPT',scope:'global',label:`AI Prompt`,before:oldText,after:newText,undo:e=>{setAiPrompt(e.before);showToast(`Undo: AI Prompt`,'info');},redo:e=>{setAiPrompt(e.after);showToast(`Redo: AI Prompt`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setAiPrompt(newText);};const setTrackInstrumentWithUndo=(trackId,instrumentId,displayName,bankNumber,programNumber)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const oldInstrumentId=track.instrumentId;const oldInstrumentName=track.instrumentName;if(oldInstrumentId===instrumentId&&oldInstrumentName===displayName)return;const entry={type:'SET_INSTRUMENT',scope:'track:'+trackId,label:`Instrument ${track.name}`,before:{instrumentId:oldInstrumentId,instrumentName:oldInstrumentName,bankNumber:track.soundfont_bank,programNumber:track.instrumentProgram},after:{instrumentId,instrumentName:displayName,bankNumber,programNumber},undo:e=>{setTrackInstrumentWithProgram(trackId,e.before.instrumentId,e.before.programNumber,e.before.instrumentName,e.before.bankNumber);showToast(`Undo: Instrument`,'info');},redo:e=>{setTrackInstrumentWithProgram(trackId,e.after.instrumentId,e.after.programNumber,e.after.instrumentName,e.after.bankNumber);showToast(`Redo: Instrument`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setTrackInstrumentWithProgram(trackId,instrumentId,programNumber,displayName,bankNumber);};const createSectionWithUndo=(trackId,section)=>{const entry={type:'CREATE_SECTION',scope:'track:'+trackId,label:`Create Section`,before:null,after:section,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,sections:(t.sections||[]).filter(s=>s.id!==e.after.id)}:t));showToast(`Undo: Create Section`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,sections:[...(t.sections||[]),e.after]}:t));showToast(`Redo: Create Section`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const createMidiWithUndo=(trackId,midiItem)=>{const entry={type:'CREATE_MIDI',scope:'track:'+trackId,label:`Create MIDI Item`,before:null,after:midiItem,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==e.after.id)}:t));showToast(`Undo: Create MIDI`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:[...(t.midiItems||[]),e.after]}:t));showToast(`Redo: Create MIDI`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const createClipWithUndo=(trackId,clip)=>{const entry={type:'CREATE_CLIP',scope:'track:'+trackId,label:`Create Audio Clip`,before:null,after:clip,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,clips:(t.clips||[]).filter(c=>c.id!==e.after.id),buffer:(t.clips||[]).filter(c=>c.id!==e.after.id)[0]?.buffer||null,startTime:(t.clips||[]).filter(c=>c.id!==e.after.id)[0]?.startTime||0,name:(t.clips||[]).filter(c=>c.id!==e.after.id)[0]?.name||t.name}:t));showToast(`Undo: Create Clip`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,clips:[...(t.clips||[]),e.after]}:t));showToast(`Redo: Create Clip`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const deleteTrackWithUndo=(trackId,trackData)=>{const entry={type:'DELETE_TRACK',scope:'global',label:`Delete Track`,before:trackData,after:null,undo:e=>{if(e.before){setTracks(prev=>[...prev,e.before]);showToast(`Undo: Delete Track`,'info');}},redo:e=>{setTracks(prev=>prev.filter(t=>t.id!==trackId));showToast(`Redo: Delete Track`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};// ── Tab System (LOOP_EDITOR_2.md §1) ── const[activeTab,setActiveTab]=useState('main');const[subTabSelectedNodeTime,setSubTabSelectedNodeTime]=useState(null);const[subTabNormVal,setSubTabNormVal]=useState(0);const[subTabGainVal,setSubTabGainVal]=useState(100);const[subTabPitchVal,setSubTabPitchVal]=useState(0);const[subTabs,setSubTabs]=useState([]);// [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...] const[sessionTabs,setSessionTabs]=useState([]);// [{id, name, tracks}, ...] const[tabContextMenu,setTabContextMenu]=useState(null);// { x, y, tabId, tabType } @@ -392,7 +392,7 @@ var ids=preToggleSnapshot?new Set(preToggleSnapshot):new Set(selectedItemIds);if const handleSweepSelectStart=(trackId,startTime,startY)=>{isSweepingRef.current=true;sweepStartRef.current=startTime;sweepTrackIdRef.current=trackId;sweepStartYRef.current=startY||0;sweepEndYRef.current=startY||0;var init={startTime,endTime:startTime};sweepSelectRef.current=init;setSweepSelect(init);// Do NOT clear selection here – wait until mouseup. // Small movement → deselect all; large movement → marquee toggle. };// ── Section / MIDI Item Drag Start ── -const handleSectionItemDragStart=(trackId,itemType,itemId,clickOffset,isDuplicate,pendingSelectedIds)=>{var curTracks=activeTracksRef.current||activeTracks;var multiIds=null;var selIds=pendingSelectedIds||selectedItemIds;if(selIds&&selIds.size>0&&selIds.has(itemId)){var selArr=Array.from(selIds);var originals={};curTracks.forEach(function(t){(t.sections||[]).forEach(function(s){if(selArr.indexOf(s.id)>=0)originals[s.id]={type:'section',start:s.start,trackId:t.id};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime,trackId:t.id};});(t.clips||[]).forEach(function(c){var cid=c.id==='default'?'default_'+t.id:c.id;if(selArr.indexOf(cid)>=0)originals[cid]={type:'clip',start:c.startTime,trackId:t.id};});});if(Object.keys(originals).length>0)multiIds=originals;}if(isDuplicate){var track=curTracks.find(function(t){return t.id===trackId;});if(!track)return;if(multiIds){var newOriginals={};updateActiveTracks(function(prev){return prev.map(function(t){var updatedSections=t.sections?t.sections.slice():[];var updatedMidi=t.midiItems?t.midiItems.slice():[];var updatedClips=t.clips?t.clips.slice():[];Object.keys(multiIds).forEach(function(oid){var info=multiIds[oid];if(info.type==='section'){var sec=(t.sections||[]).find(function(s){return s.id===oid;});if(sec){var rearrangeNewId='sec_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedSections.push({...sec,id:rearrangeNewId,name:sec.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'section',start:sec.start};}}else if(info.type==='midiItem'){var mid=(t.midiItems||[]).find(function(m){return m.id===oid;});if(mid){var rearrangeNewId='midi_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedMidi.push({...mid,id:rearrangeNewId,name:mid.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'midiItem',start:mid.startTime};}}else if(info.type==='clip'){var clip=(t.clips||[]).find(function(c){return c.id===oid||'default_'+t.id===oid;});if(clip){var rearrangeNewId='clip_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedClips.push({...clip,id:rearrangeNewId,startTime:clip.startTime,name:clip.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'clip',start:clip.startTime};}}});return{...t,sections:updatedSections,midiItems:updatedMidi,clips:updatedClips};});});var newItemId=Object.keys(newOriginals)[0]||itemId;var origPos=newOriginals[newItemId]||{start:0};setDraggedSectionItem({trackId,itemType,itemId:newItemId,clickOffset,multiIds:newOriginals,originalPositions:{[newItemId]:origPos.start}});}else{var items=itemType==='section'?track.sections||[]:track.midiItems||[];var item=items.find(function(it){return it.id===itemId;});if(!item)return;var rearrangeNewId=itemType+'_dup_'+Date.now();var newItem={...item,id:rearrangeNewId,name:item.name+' (Copy)'};updateActiveTracks(function(prev){return prev.map(function(t){if(t.id!==trackId)return t;var updated=itemType==='section'?[...(t.sections||[]),newItem]:[...(t.midiItems||[]),newItem];return itemType==='section'?{...t,sections:updated}:{...t,midiItems:updated};});});setDraggedSectionItem({trackId,itemType,itemId:rearrangeNewId,clickOffset,isDuplicate:false,originalPositions:{[rearrangeNewId]:itemType==='section'?item.start:item.startTime}});}}else{var curTrk=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===trackId):null;var its=itemType==='section'?curTrk?.sections||[]:curTrk?.midiItems||[];var it=its.find(function(x){return x.id===itemId;});var origPos=it?itemType==='section'?it.start:it.startTime:0;setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds,originalPositions:{[itemId]:origPos}});}};handleSectionItemDragStartRef.current=handleSectionItemDragStart;// ── Section / MIDI Item Resize Start ── +const handleSectionItemDragStart=(trackId,itemType,itemId,clickOffset,isDuplicate,pendingSelectedIds)=>{var curTracks=activeTracksRef.current||activeTracks;var multiIds=null;var selIds=pendingSelectedIds||selectedItemIds;if(selIds&&selIds.size>0&&selIds.has(itemId)){var selArr=Array.from(selIds);var originals={};curTracks.forEach(function(t){(t.sections||[]).forEach(function(s){if(selArr.indexOf(s.id)>=0)originals[s.id]={type:'section',start:s.start,trackId:t.id};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime,trackId:t.id};});(t.clips||[]).forEach(function(c){var cid=c.id==='default'?'default_'+t.id:c.id;if(selArr.indexOf(cid)>=0)originals[cid]={type:'clip',start:c.startTime,trackId:t.id};});});if(Object.keys(originals).length>0)multiIds=originals;}if(isDuplicate){var track=curTracks.find(function(t){return t.id===trackId;});if(!track)return;if(multiIds){var newOriginals={};var dupBeforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(function(prev){return prev.map(function(t){var updatedSections=t.sections?t.sections.slice():[];var updatedMidi=t.midiItems?t.midiItems.slice():[];var updatedClips=t.clips?t.clips.slice():[];Object.keys(multiIds).forEach(function(oid){var info=multiIds[oid];if(info.type==='section'){var sec=(t.sections||[]).find(function(s){return s.id===oid;});if(sec){var rearrangeNewId='sec_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedSections.push({...sec,id:rearrangeNewId,name:sec.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'section',start:sec.start};}}else if(info.type==='midiItem'){var mid=(t.midiItems||[]).find(function(m){return m.id===oid;});if(mid){var rearrangeNewId='midi_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedMidi.push({...mid,id:rearrangeNewId,name:mid.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'midiItem',start:mid.startTime};}}else if(info.type==='clip'){var clip=(t.clips||[]).find(function(c){return c.id===oid||'default_'+t.id===oid;});if(clip){var rearrangeNewId='clip_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedClips.push({...clip,id:rearrangeNewId,startTime:clip.startTime,name:clip.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'clip',start:clip.startTime};}}});return{...t,sections:updatedSections,midiItems:updatedMidi,clips:updatedClips};});});var newItemId=Object.keys(newOriginals)[0]||itemId;var origPos=newOriginals[newItemId]||{start:0};setDraggedSectionItem({trackId,itemType,itemId:newItemId,clickOffset,multiIds:newOriginals,originalPositions:{[newItemId]:origPos.start},beforeSnap:dupBeforeSnap});}else{var items=itemType==='section'?track.sections||[]:track.midiItems||[];var item=items.find(function(it){return it.id===itemId;});if(!item)return;var rearrangeNewId=itemType+'_dup_'+Date.now();var newItem={...item,id:rearrangeNewId,name:item.name+' (Copy)'};var singleDupBeforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(function(prev){return prev.map(function(t){if(t.id!==trackId)return t;var updated=itemType==='section'?[...(t.sections||[]),newItem]:[...(t.midiItems||[]),newItem];return itemType==='section'?{...t,sections:updated}:{...t,midiItems:updated};});});setDraggedSectionItem({trackId,itemType,itemId:rearrangeNewId,clickOffset,isDuplicate:false,originalPositions:{[rearrangeNewId]:itemType==='section'?item.start:item.startTime},beforeSnap:singleDupBeforeSnap});}}else{var curTrk=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===trackId):null;var its=itemType==='section'?curTrk?.sections||[]:curTrk?.midiItems||[];var it=its.find(function(x){return x.id===itemId;});var origPos=it?itemType==='section'?it.start:it.startTime:0;var beforeSnap=captureTrackSnapshot(trackId);setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds,originalPositions:{[itemId]:origPos},beforeSnap});}};handleSectionItemDragStartRef.current=handleSectionItemDragStart;// ── Section / MIDI Item Resize Start ── const handleSectionItemResizeStart=(trackId,itemType,itemId,side,clickTime)=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===trackId);if(!track)return;const items=itemType==='section'?track.sections:track.midiItems;const item=(items||[]).find(it=>it.id===itemId);if(!item)return;const start=itemType==='section'?item.start:item.startTime;setResizedSectionItem({trackId,itemType,itemId,side,originalStart:start,originalDuration:item.duration});};// ── Document-level mousemove/mouseup for Section/MIDI item drag ── useEffect(()=>{const handleMouseMove=e=>{const drag=draggedSectionItemRef.current;if(!drag)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);const beatSec=60.0/(parseInt(bpm)||120);const secondsPerBar=beatSec*4;const marginBar=maxDurationRef.current-secondsPerBar;const rawStart=Math.max(0,Math.min(time-drag.clickOffset,marginBar));const newStart=snapTime(rawStart,snapValueRef.current,bpmRef.current);const itemPx=newStart*zoom;const keepMargin=80;if(itemPx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=itemPx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(itemPxn+1);}// Compute target track from mouse Y position var allTrks=activeTracksRef.current||[];var mouseY=e.clientY-rect.top;var trackTop=0;var targetIdx=-1;for(var ti=0;ti=trackTop&&mouseY=allTrks.length){for(var ai=allTrks.length;ai<=needIdx;ai++){var exists=outTracks.some(function(ot){return ot.id===(ai+1).toString();});if(!exists){var colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];outTracks.push({id:(ai+1).toString(),name:'Track '+(ai+1),buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[ai%colors.length],clips:[],midiItems:[],sections:[],markers:[],isArmed:false,height:140});}}}});updateActiveTracks(function(prev){var trkIds=prev.map(function(tr){return tr.id;});var merged=outTracks.length>0?prev.concat(outTracks.filter(function(ot){return!trkIds.includes(ot.id);})):prev;var allIds=merged.map(function(tr){return tr.id;});return merged.map(function(t){var resS=(t.sections||[]).slice();var resM=(t.midiItems||[]).slice();var resC=(t.clips||[]).slice();Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var newVal=inf.start+delta;var oIdx=allIds.indexOf(inf.trackId);if(oIdx<0)oIdx=baseIdx;var needIdx=Math.max(0,oIdx+crossOffset);var targetTid=needIdx{const drag=draggedSectionItemRef.current;if(!drag)return;var changed=false;var trackId=drag.trackId;var beforeSnap=captureTrackSnapshot(trackId);var origs=drag.originalPositions||{};updateActiveTracks(function(prev){return prev.map(function(t){var allItemIds=Object.keys(origs);var updatedSections=(t.sections||[]).slice();var updatedMidi=(t.midiItems||[]).slice();var hasChange=false;for(var i=0;i0.001){hasChange=true;changed=true;if(sec)item.start=origStart;if(mid)item.startTime=origStart;}}}}if(!hasChange)return t;return{...t,sections:updatedSections,midiItems:updatedMidi};});});if(changed){var afterSnap=captureTrackSnapshot(trackId);pushAction('MOVE_ITEM',trackId,beforeSnap,afterSnap);}if(drag.itemType==='midiItem'){let finalTrackId=null;const curTrks=activeTracksRef.current||activeTracks;for(const t of curTrks){if((t.midiItems||[]).some(m=>m.id===drag.itemId)){finalTrackId=t.id;break;}}if(finalTrackId){const targetTrack=curTrks.find(t=>t.id===finalTrackId);if(targetTrack){setSubTabs(prev=>prev.map(st=>{if(st.target_id===drag.itemId){return{...st,trackId:finalTrackId,instrumentProgram:targetTrack.instrumentProgram!==undefined?targetTrack.instrumentProgram:st.instrumentProgram,instrumentName:targetTrack.instrumentName||st.instrumentName};}return st;}));}}}setDraggedSectionItem(null);showToast('Đã di chuyển '+(drag.itemType==='section'?'section':'MIDI item')+'.','success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag ── +updateActiveTracks(function(prev){let movedItem=null;for(let track of prev){const sec=(track.sections||[]).find(function(it){return it.id===drag.itemId;});const mid=(track.midiItems||[]).find(function(it){return it.id===drag.itemId;});if(sec||mid){movedItem=sec||mid;break;}}return prev.map(function(tr){var its=drag.itemType==='section'?(tr.sections||[]).filter(function(it){return it.id!==drag.itemId;}):(tr.midiItems||[]).filter(function(it){return it.id!==drag.itemId;});if(tr.id===targetTrackId&&movedItem){its.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}return drag.itemType==='section'?{...tr,sections:its}:{...tr,midiItems:its};});});}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;var changed=false;var trackId=drag.trackId;var beforeSnap=drag.beforeSnap;var origs=drag.originalPositions||{};updateActiveTracks(function(prev){return prev.map(function(t){var allItemIds=Object.keys(origs);var updatedSections=(t.sections||[]).slice();var updatedMidi=(t.midiItems||[]).slice();var hasChange=false;for(var i=0;i0.001){hasChange=true;changed=true;}}}}if(!hasChange)return t;return{...t,sections:updatedSections,midiItems:updatedMidi};});});if(changed||drag.beforeSnap){var afterSnap=captureTrackSnapshot(trackId);pushAction('MOVE_ITEM',trackId,drag.beforeSnap||beforeSnap,afterSnap);}if(drag.itemType==='midiItem'){let finalTrackId=null;const curTrks=activeTracksRef.current||activeTracks;for(const t of curTrks){if((t.midiItems||[]).some(m=>m.id===drag.itemId)){finalTrackId=t.id;break;}}if(finalTrackId){const targetTrack=curTrks.find(t=>t.id===finalTrackId);if(targetTrack){setSubTabs(prev=>prev.map(st=>{if(st.target_id===drag.itemId){return{...st,trackId:finalTrackId,instrumentProgram:targetTrack.instrumentProgram!==undefined?targetTrack.instrumentProgram:st.instrumentProgram,instrumentName:targetTrack.instrumentName||st.instrumentName};}return st;}));}}}setDraggedSectionItem(null);showToast('Đã di chuyển '+(drag.itemType==='section'?'section':'MIDI item')+'.','success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag ── useEffect(()=>{const handleMouseMove=e=>{var pd=pendingDragRef.current;if(!pd)return;var dx=e.clientX-pd.startX;if(Math.abs(dx)>5){var pdSnap=pendingDragRef.current;pendingDragRef.current=null;if(handleSectionItemDragStartRef.current)handleSectionItemDragStartRef.current(pdSnap.trackId,pdSnap.itemType,pdSnap.itemId,pdSnap.clickOffset,true,pdSnap.selectedIds);}};document.addEventListener('mousemove',handleMouseMove);var handleMouseUp=function(){pendingDragRef.current=null;};document.addEventListener('mouseup',handleMouseUp);return function(){document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);pendingDragRef.current=null;};},[]);// ── Document-level mousemove/mouseup for Section/MIDI item resize ── useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const snappedDuration=snapValueRef.current!=='free'?snapTime(clampedTime-resize.originalStart,snapValueRef.current,bpm):clampedTime-resize.originalStart;const newDuration=Math.max(0.1,snappedDuration);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePxn+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ── useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;captureSelectionUndo();if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);if(Math.abs(end-start)<0.02){setSelectedItemIds(new Set());setSweepSelect(null);pushSelectionUndo();return;}const curTracks=activeTracksRef.current||[];const found=new Set();curTracks.forEach(t=>{(t.midiItems||[]).forEach(m=>{if(m.startTimestart){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.startstart){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTimestart){const cid=c.id==='default'?'default_'+t.id:c.id;found.add(cid);}});});setSelectedItemIds(prev=>{const next=new Set(prev);found.forEach(id=>{if(next.has(id))next.delete(id);else next.add(id);});return next;});setSweepSelect(null);sweepTrackIdRef.current=null;pushSelectionUndo();}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);const handleSelectRange=(start,end,reset)=>{captureSelectionUndo();const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}setSelectionCleared(false);pushSelectionUndo();};const handleSelectionInputChange=(field,val)=>{captureSelectionUndo();const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){if(field==='start'){setLocalSelectionStart(numericVal);}else{setLocalSelectionEnd(numericVal);}}else{if(field==='start'){setSelectionStart(numericVal);}else{setSelectionEnd(numericVal);}}pushSelectionUndo();};const selectionStats=useMemo(()=>{if(selLeft===null||selRight===null){return{start:0,end:0,length:0};}const s=Math.min(selLeft,selRight);const e=Math.max(selLeft,selRight);return{start:parseFloat(s.toFixed(3)),end:parseFloat(e.toFixed(3)),length:parseFloat((e-s).toFixed(3))};},[selLeft,selRight]);// ── Handle Drag (selection resize) ──