fix: selectedItemIds snapshot includes toggled item for drag
React batches setSelectedItemIds, so when handleSetPendingDrag snapshots selectedItemIds, the just-toggled item isn't included yet. Fix: pass wasAlreadySelected boolean from TimelineTrack mousedown; handleSetPendingDrag manually applies the toggle (add/delete) to the snapshot so drag uses correct selection.
This commit is contained in:
+17
-10
@@ -1084,13 +1084,14 @@ const WaveformLane = ({
|
||||
e.stopPropagation();
|
||||
if (e.ctrlKey) {
|
||||
// Ctrl+Click: toggle selection only, never start drag immediately
|
||||
if (selectedItemIds && selectedItemIds.has(hitItem.id)) {
|
||||
var ctrlWasSelected = selectedItemIds && selectedItemIds.has(hitItem.id);
|
||||
if (ctrlWasSelected) {
|
||||
if (onDeselectItem) onDeselectItem(hitItem.id);
|
||||
} else if (onAddToSelection) {
|
||||
onAddToSelection(hitItem.id);
|
||||
}
|
||||
// Store pending drag so mousemove can start copy-drag
|
||||
if (onSetPendingDrag) onSetPendingDrag(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e);
|
||||
if (onSetPendingDrag) onSetPendingDrag(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e, ctrlWasSelected);
|
||||
} else {
|
||||
// No modifier: start drag immediately (single or multi move)
|
||||
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, false);
|
||||
@@ -1152,12 +1153,13 @@ const WaveformLane = ({
|
||||
if (e.ctrlKey) {
|
||||
// Ctrl+Click: toggle selection, store pending drag
|
||||
var clipCanonicalId = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
|
||||
if (selectedItemIds && selectedItemIds.has(clipCanonicalId)) {
|
||||
var ctrlClipSelected = selectedItemIds && selectedItemIds.has(clipCanonicalId);
|
||||
if (ctrlClipSelected) {
|
||||
if (onDeselectItem) onDeselectItem(clipCanonicalId);
|
||||
} else if (onAddToSelection) {
|
||||
onAddToSelection(clipCanonicalId);
|
||||
}
|
||||
if (onSetPendingDrag) onSetPendingDrag(track.id, 'clip', clipCanonicalId, time - clickedClip.startTime, e.nativeEvent || e);
|
||||
if (onSetPendingDrag) onSetPendingDrag(track.id, 'clip', clipCanonicalId, time - clickedClip.startTime, e.nativeEvent || e, ctrlClipSelected);
|
||||
} else {
|
||||
// Alt+Click: move immediately
|
||||
if (onClipDragStart) onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, false);
|
||||
@@ -11664,8 +11666,11 @@ const App = () => {
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e) => {
|
||||
pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY };
|
||||
const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e, wasAlreadySelected) => {
|
||||
var snap = new Set(selectedItemIds);
|
||||
if (wasAlreadySelected) snap.delete(itemId);
|
||||
else snap.add(itemId);
|
||||
pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY, selectedIds: snap };
|
||||
};
|
||||
|
||||
// ── Sweep Select ──
|
||||
@@ -11680,11 +11685,12 @@ const App = () => {
|
||||
};
|
||||
|
||||
// ── Section / MIDI Item Drag Start ──
|
||||
const handleSectionItemDragStart = (trackId, itemType, itemId, clickOffset, isDuplicate) => {
|
||||
const handleSectionItemDragStart = (trackId, itemType, itemId, clickOffset, isDuplicate, pendingSelectedIds) => {
|
||||
var curTracks = activeTracksRef.current || activeTracks;
|
||||
var multiIds = null;
|
||||
if (selectedItemIds && selectedItemIds.size > 0 && selectedItemIds.has(itemId)) {
|
||||
var selArr = Array.from(selectedItemIds);
|
||||
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) {
|
||||
@@ -11876,8 +11882,9 @@ const App = () => {
|
||||
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(pd.trackId, pd.itemType, pd.itemId, pd.clickOffset, true);
|
||||
if (handleSectionItemDragStartRef.current) handleSectionItemDragStartRef.current(pdSnap.trackId, pdSnap.itemType, pdSnap.itemId, pdSnap.clickOffset, true, pdSnap.selectedIds);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
@@ -52,12 +52,12 @@ if(e.shiftKey){e.preventDefault();e.stopPropagation();const currentAnchor=getLoc
|
||||
const isLocal=selectionMode==='local'&&localSelectionTrackId===track.id;if(isLocal&&localSelLeft!==null&&localSelRight!==null){const leftPx=localSelLeft*zoom;const rightPx=localSelRight*zoom;const distToLeft=Math.abs(x-leftPx);const distToRight=Math.abs(x-rightPx);if(distToLeft<=5){e.preventDefault();e.stopPropagation();if(onSelectionEdgeDragStart)onSelectionEdgeDragStart(e,track.id,'left');return;}else if(distToRight<=5){e.preventDefault();e.stopPropagation();if(onSelectionEdgeDragStart)onSelectionEdgeDragStart(e,track.id,'right');return;}}// Check if time-stretching (Alt + Right Edge)
|
||||
const toleranceSec=8/zoom;const rightEdgeClip=clips.find(c=>{const duration=c.buffer.duration/(c.speed||1.0);return Math.abs(time-(c.startTime+duration))<=toleranceSec;});if(rightEdgeClip&&e.altKey){e.preventDefault();e.stopPropagation();if(onClipStretchStart){onClipStretchStart(track.id,rightEdgeClip.id,time);}return;}// Check section/MIDI item edge for resize, then body for drag
|
||||
const secItems=track.sections||[];const midiItems=track.midiItems||[];const secTol=8/zoom;let hitItem=null;let hitEdge=null;for(const sec of secItems){if(Math.abs(time-sec.start)<=secTol){hitItem={type:'section',id:sec.id,start:sec.start,dur:sec.duration};hitEdge='left';break;}if(Math.abs(time-(sec.start+sec.duration))<=secTol){hitItem={type:'section',id:sec.id,start:sec.start,dur:sec.duration};hitEdge='right';break;}}if(!hitItem){for(const midi of midiItems){if(Math.abs(time-midi.startTime)<=secTol){hitItem={type:'midiItem',id:midi.id,start:midi.startTime,dur:midi.duration};hitEdge='left';break;}if(Math.abs(time-(midi.startTime+midi.duration))<=secTol){hitItem={type:'midiItem',id:midi.id,start:midi.startTime,dur:midi.duration};hitEdge='right';break;}}}if(hitItem&&hitEdge){e.preventDefault();e.stopPropagation();if(onSectionItemResizeStart)onSectionItemResizeStart(track.id,hitItem.type,hitItem.id,hitEdge,time);return;}if(!hitItem){for(const sec of secItems){if(time>=sec.start&&time<sec.start+sec.duration){hitItem={type:'section',id:sec.id,start:sec.start};break;}}}if(!hitItem){for(const midi of midiItems){if(time>=midi.startTime&&time<midi.startTime+midi.duration){hitItem={type:'midiItem',id:midi.id,start:midi.startTime};break;}}}if(hitItem&&!e.altKey&&!e.shiftKey){e.preventDefault();e.stopPropagation();if(e.ctrlKey){// Ctrl+Click: toggle selection only, never start drag immediately
|
||||
if(selectedItemIds&&selectedItemIds.has(hitItem.id)){if(onDeselectItem)onDeselectItem(hitItem.id);}else if(onAddToSelection){onAddToSelection(hitItem.id);}// Store pending drag so mousemove can start copy-drag
|
||||
if(onSetPendingDrag)onSetPendingDrag(track.id,hitItem.type,hitItem.id,time-hitItem.start,e.nativeEvent||e);}else{// No modifier: start drag immediately (single or multi move)
|
||||
var ctrlWasSelected=selectedItemIds&&selectedItemIds.has(hitItem.id);if(ctrlWasSelected){if(onDeselectItem)onDeselectItem(hitItem.id);}else if(onAddToSelection){onAddToSelection(hitItem.id);}// Store pending drag so mousemove can start copy-drag
|
||||
if(onSetPendingDrag)onSetPendingDrag(track.id,hitItem.type,hitItem.id,time-hitItem.start,e.nativeEvent||e,ctrlWasSelected);}else{// No modifier: start drag immediately (single or multi move)
|
||||
if(onSectionItemDragStart)onSectionItemDragStart(track.id,hitItem.type,hitItem.id,time-hitItem.start,false);}return;}const clickedClip=clips.find(c=>time>=c.startTime&&time<c.startTime+c.buffer.duration/(c.speed||1.0));// Set selected clip ID
|
||||
if(clickedClip){setSelectedClipId({trackId:track.id,clipId:clickedClip.id==='default'?'default_'+track.id:clickedClip.id});}else{setSelectedClipId(null);}if(activeTool==='pen'&&!e.ctrlKey){if(clickedClip){e.preventDefault();e.stopPropagation();if(onEditClipInSubTab){onEditClipInSubTab(track.id,clickedClip.id);}}return;}if(activeTool==='razor'&&!e.ctrlKey){if(clickedClip){e.preventDefault();e.stopPropagation();if(onSplitTrackAtTime){onSplitTrackAtTime(track.id,clickedClip.id,time);}}return;}if(activeTool==='grab'){if(onTrackLaneMouseDown){onTrackLaneMouseDown(track.id,time,e);}if(clickedClip){e.preventDefault();e.stopPropagation();if(onClipDragStart){onClipDragStart(track.id,clickedClip.id,time-clickedClip.startTime,e.ctrlKey);}}else{onPlayheadSet(time);}return;}// Check for click drag clip (Alt to move, Ctrl to duplicate)
|
||||
if(clickedClip&&(e.altKey||e.ctrlKey)){e.preventDefault();e.stopPropagation();if(e.ctrlKey){// Ctrl+Click: toggle selection, store pending drag
|
||||
var clipCanonicalId=clickedClip.id==='default'?'default_'+track.id:clickedClip.id;if(selectedItemIds&&selectedItemIds.has(clipCanonicalId)){if(onDeselectItem)onDeselectItem(clipCanonicalId);}else if(onAddToSelection){onAddToSelection(clipCanonicalId);}if(onSetPendingDrag)onSetPendingDrag(track.id,'clip',clipCanonicalId,time-clickedClip.startTime,e.nativeEvent||e);}else{// Alt+Click: move immediately
|
||||
var clipCanonicalId=clickedClip.id==='default'?'default_'+track.id:clickedClip.id;var ctrlClipSelected=selectedItemIds&&selectedItemIds.has(clipCanonicalId);if(ctrlClipSelected){if(onDeselectItem)onDeselectItem(clipCanonicalId);}else if(onAddToSelection){onAddToSelection(clipCanonicalId);}if(onSetPendingDrag)onSetPendingDrag(track.id,'clip',clipCanonicalId,time-clickedClip.startTime,e.nativeEvent||e,ctrlClipSelected);}else{// Alt+Click: move immediately
|
||||
if(onClipDragStart)onClipDragStart(track.id,clickedClip.id,time-clickedClip.startTime,false);}return;}// Ctrl+Click on empty space: sweep select (deselect all via onSweepSelectStart)
|
||||
if(e.ctrlKey&&!clickedClip&&!hitItem){e.preventDefault();e.stopPropagation();if(onSweepSelectStart)onSweepSelectStart(track.id,time);return;}onPlayheadSet(time);if(onTrackLaneMouseDown){onTrackLaneMouseDown(track.id,time,e);}e.stopPropagation();},onDoubleClick:e=>{const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Check double-click on section first
|
||||
const dblSecItems=track.sections||[];let dblSecHit=null;for(const sec of dblSecItems){if(time>=sec.start&&time<sec.start+sec.duration){dblSecHit=sec;break;}}if(dblSecHit){e.preventDefault();e.stopPropagation();if(onEditSectionInTab)onEditSectionInTab(track.id,dblSecHit.id);return;}// Check double-click on MIDI item next
|
||||
@@ -330,12 +330,12 @@ if(t.id===drag.trackId&&drag.trackId!==targetTrackId){const updatedClips=(t.clip
|
||||
if(t.id===targetTrackId){const existingClips=t.clips&&t.clips.length>0?t.clips:t.buffer?[{id:'default_'+t.id,buffer:t.buffer,startTime:t.startTime||0,name:t.name}]:[];const hasClip=existingClips.some(c=>c.id===drag.clipId);let updatedClips;if(hasClip){updatedClips=existingClips.map(c=>c.id===drag.clipId?{...c,startTime:newStart}:c);}else{updatedClips=[...existingClips,{id:drag.clipId,buffer:drag.buffer,startTime:newStart,name:drag.name}];}return{...t,clips:updatedClips,buffer:updatedClips[0].buffer,startTime:updatedClips[0].startTime,name:updatedClips[0].name};}return t;}));if(drag.trackId!==targetTrackId){setDraggedClip(prev=>({...prev,trackId:targetTrackId}));}};const handleMouseUp=()=>{const drag=draggedClipRef.current;if(!drag)return;const afterSnap=captureTrackSnapshotRef.current(drag.trackId);pushAction('MOVE_CLIP',drag.trackId,drag.beforeSnap,afterSnap);setDraggedClip(null);showToast('Đã di chuyển clip.','success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);// Document-level mousemove/mouseup for clip stretching
|
||||
useEffect(()=>{const handleMouseMove=e=>{const stretch=stretchedClipRef.current;if(!stretch)return;autoScrollTimeline(e.clientX);const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=mouseX/zoom;const secPerBar=60.0/(parseInt(bpmRef.current)||120)*4;const marginBar=maxDurationRef.current-secPerBar;const maxEnd=Math.min(time,marginBar);const newDuration=Math.max(0.1,maxEnd-stretch.startTime);const speedRatio=stretch.originalDuration/newDuration;updateActiveTracks(prev=>prev.map(t=>{if(t.id===stretch.trackId){const updatedClips=(t.clips||[]).map(c=>{if(c.id===stretch.clipId){return{...c,speed:speedRatio};}return c;});return{...t,clips:updatedClips,buffer:updatedClips[0]?.buffer,startTime:updatedClips[0]?.startTime||0,speed:updatedClips[0]?.speed||1.0};}return t;}));};const handleMouseUp=()=>{const stretch=stretchedClipRef.current;if(!stretch)return;const afterSnap=captureTrackSnapshotRef.current(stretch.trackId);pushAction('STRETCH_CLIP',stretch.trackId,stretch.beforeSnap,afterSnap);setStretchedClip(null);showToast('Đã giãn thời gian clip.','success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);// ── Deselect single item ──
|
||||
const handleDeselectItem=itemId=>{setSelectedItemIds(prev=>{var next=new Set(prev);next.delete(itemId);return next;});};// ── Add item to selectedItemIds ──
|
||||
const handleAddToSelection=itemId=>{setSelectedItemIds(prev=>{var next=new Set(prev);next.add(itemId);return next;});};const handleSetPendingDrag=(trackId,itemType,itemId,clickOffset,e)=>{pendingDragRef.current={trackId,itemType,itemId,clickOffset,startX:e.clientX,startY:e.clientY};};// ── Sweep Select ──
|
||||
const handleAddToSelection=itemId=>{setSelectedItemIds(prev=>{var next=new Set(prev);next.add(itemId);return next;});};const handleSetPendingDrag=(trackId,itemType,itemId,clickOffset,e,wasAlreadySelected)=>{var snap=new Set(selectedItemIds);if(wasAlreadySelected)snap.delete(itemId);else snap.add(itemId);pendingDragRef.current={trackId,itemType,itemId,clickOffset,startX:e.clientX,startY:e.clientY,selectedIds:snap};};// ── Sweep Select ──
|
||||
const handleSweepSelectStart=(trackId,startTime)=>{isSweepingRef.current=true;sweepStartRef.current=startTime;sweepTrackIdRef.current=trackId;var init={startTime,endTime:startTime};sweepSelectRef.current=init;setSweepSelect(init);setSelectedItemIds(new Set());};// ── Section / MIDI Item Drag Start ──
|
||||
const handleSectionItemDragStart=(trackId,itemType,itemId,clickOffset,isDuplicate)=>{var curTracks=activeTracksRef.current||activeTracks;var multiIds=null;if(selectedItemIds&&selectedItemIds.size>0&&selectedItemIds.has(itemId)){var selArr=Array.from(selectedItemIds);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};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime};});(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};});});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 newId='sec_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedSections.push({...sec,id:newId,name:sec.name+' (Copy)'});newOriginals[newId]={type:'section',start:sec.start};}}else if(info.type==='midiItem'){var mid=(t.midiItems||[]).find(function(m){return m.id===oid;});if(mid){var newId='midi_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedMidi.push({...mid,id:newId,name:mid.name+' (Copy)'});newOriginals[newId]={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 newId='clip_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedClips.push({...clip,id:newId,startTime:clip.startTime,name:clip.name+' (Copy)'});newOriginals[newId]={type:'clip',start:clip.startTime};}}});var changed=t.sections&&t.sections.length!==updatedSections.length;return t.id===trackId?{...t,sections:updatedSections,midiItems:updatedMidi,clips:updatedClips}:t;});});setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds:newOriginals});}else{var items=itemType==='section'?track.sections||[]:track.midiItems||[];var item=items.find(function(it){return it.id===itemId;});if(!item)return;var newId=itemType+'_dup_'+Date.now();var newItem={...item,id:newId,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:newId,clickOffset,isDuplicate:false});}return;}setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds});};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};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime};});(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};});});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 newId='sec_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedSections.push({...sec,id:newId,name:sec.name+' (Copy)'});newOriginals[newId]={type:'section',start:sec.start};}}else if(info.type==='midiItem'){var mid=(t.midiItems||[]).find(function(m){return m.id===oid;});if(mid){var newId='midi_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedMidi.push({...mid,id:newId,name:mid.name+' (Copy)'});newOriginals[newId]={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 newId='clip_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedClips.push({...clip,id:newId,startTime:clip.startTime,name:clip.name+' (Copy)'});newOriginals[newId]={type:'clip',start:clip.startTime};}}});var changed=t.sections&&t.sections.length!==updatedSections.length;return t.id===trackId?{...t,sections:updatedSections,midiItems:updatedMidi,clips:updatedClips}:t;});});setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds:newOriginals});}else{var items=itemType==='section'?track.sections||[]:track.midiItems||[];var item=items.find(function(it){return it.id===itemId;});if(!item)return;var newId=itemType+'_dup_'+Date.now();var newItem={...item,id:newId,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:newId,clickOffset,isDuplicate:false});}return;}setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds});};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(itemPx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,itemPx-keepMargin);setCanvasRedrawCount(n=>n+1);}const targetTrackId=hoveredTrackIdRef.current||drag.trackId;updateActiveTracks(prev=>{let movedItem=null;for(let track of prev){const items=drag.itemType==='section'?track.sections||[]:track.midiItems||[];const found=items.find(it=>it.id===drag.itemId);if(found){movedItem=found;break;}}return prev.map(t=>{if(drag.multiIds){var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;var midSections=(t.sections||[]).slice();var midMidis=(t.midiItems||[]).slice();Object.keys(drag.multiIds).forEach(function(mid){var info=drag.multiIds[mid];var newVal=info.start+delta;if(info.type==='section'){var idx=midSections.findIndex(function(s){return s.id===mid;});if(idx>=0)midSections[idx]={...midSections[idx],start:Math.max(0,newVal)};}else if(info.type==='midiItem'){var idx=midMidis.findIndex(function(m){return m.id===mid;});if(idx>=0)midMidis[idx]={...midMidis[idx],startTime:Math.max(0,newVal)};}});return t.id===targetTrackId?{...t,sections:midSections,midiItems:midMidis}:{...t,sections:t.id===drag.trackId?midSections:t.sections,midiItems:t.id===drag.trackId?midMidis:t.midiItems};}const items=drag.itemType==='section'?t.sections||[]:t.midiItems||[];const updatedItems=items.filter(it=>it.id!==drag.itemId);if(t.id===targetTrackId){if(movedItem){updatedItems.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}else{if(drag.itemType==='section'){updatedItems.push({id:drag.itemId,name:'Section',start:newStart,duration:4*secondsPerBar,color:'#06b6d4'});}else{updatedItems.push({id:drag.itemId,name:'MIDI Item',startTime:newStart,duration:4*secondsPerBar,notes:[],color:'#a78bfa'});}}}return drag.itemType==='section'?{...t,sections:updatedItems}:{...t,midiItems:updatedItems};});});if(drag.trackId!==targetTrackId){setDraggedSectionItem(prev=>({...prev,trackId:targetTrackId}));}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;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){pendingDragRef.current=null;if(handleSectionItemDragStartRef.current)handleSectionItemDragStartRef.current(pd.trackId,pd.itemType,pd.itemId,pd.clickOffset,true);}};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=>{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(edgePx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,edgePx-keepMargin);setCanvasRedrawCount(n=>n+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;if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);const curTracks=activeTracksRef.current||[];const found=new Set();curTracks.forEach(t=>{(t.midiItems||[]).forEach(m=>{if(m.startTime<end&&m.startTime+m.duration>start){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.start<end&&s.start+s.duration>start){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTime<end&&c.startTime+dur>start){const cid=c.id==='default'?'default_'+t.id:c.id;found.add(cid);}});});setSelectedItemIds(found);setSweepSelect(null);sweepTrackIdRef.current=null;}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);const handleSelectRange=(start,end,reset)=>{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);}// LOOP_EDITOR_2.md §4.2: new selection = enable looping
|
||||
setSelectionCleared(false);};const handleSelectionInputChange=(field,val)=>{const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){// Editing local selection directly
|
||||
|
||||
Reference in New Issue
Block a user