fix: rewrite drag handler with stable crossOffset + outTracks merge

This commit is contained in:
2026-07-29 11:13:31 +07:00
parent e40fdf15ad
commit d4262acb9f
3 changed files with 91 additions and 86 deletions
+79 -81
View File
@@ -12061,102 +12061,100 @@ const App = () => {
}
if (targetIdx < 0) targetIdx = mouseY < trackTop ? 0 : Math.max(0, allTrks.length - 1);
var targetTrackId = allTrks[targetIdx] ? allTrks[targetIdx].id : drag.trackId;
// Use ORIGINAL trackId (never changes during drag) for crossOffset math
// Use ORIGINAL trackId from drag start for crossOffset math (never changes)
var dragBaseTrackId = drag.multiIds && drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].trackId : drag.trackId;
updateActiveTracks(prev => {
var trkIds = prev.map(function(tr) { return tr.id; });
var baseIdx = trkIds.indexOf(dragBaseTrackId);
if (baseIdx < 0) baseIdx = 0;
var tIdx = trkIds.indexOf(targetTrackId);
if (tIdx < 0) tIdx = prev.length - 1;
if (tIdx < 0) tIdx = 0;
var crossOffset = tIdx - baseIdx;
let movedItem = null;
for (let track of prev) {
const sec = (track.sections || []).find(it => it.id === drag.itemId);
const mid = (track.midiItems || []).find(it => it.id === drag.itemId);
if (sec || mid) { movedItem = sec || mid; break; }
}
if (drag.multiIds) {
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
var delta = newStart - dragOrigStart;
// Ensure enough tracks exist for all displaced items
var maxTrg = 0;
Object.keys(drag.multiIds).forEach(function(mid) {
var inf = drag.multiIds[mid];
var origIdx = trkIds.indexOf(inf.trackId);
if (origIdx < 0) origIdx = baseIdx;
var trg = Math.max(0, origIdx + crossOffset);
if (trg > maxTrg) maxTrg = trg;
});
var tracksToAdd = [];
while (prev.length <= maxTrg) {
var newId = (prev.length + tracksToAdd.length + 1).toString();
var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
tracksToAdd.push({ id: newId, name: 'Track ' + newId, buffer: null, startTime: 0, volumeDb: 0, pan: 0, muted: false, solo: false, color: colors[(prev.length + tracksToAdd.length) % colors.length], clips: [], midiItems: [], sections: [], markers: [], isArmed: false, height: 140 });
// Pre-compute crossOffset from current allTrks (stable per mousemove)
var baseIdx = allTrks.findIndex(function(tr) { return tr.id === dragBaseTrackId; });
if (baseIdx < 0) baseIdx = 0;
var trgIdx = allTrks.findIndex(function(tr) { return tr.id === targetTrackId; });
if (trgIdx < 0) trgIdx = allTrks.length - 1;
if (trgIdx < 0) trgIdx = 0;
var crossOffset = trgIdx - baseIdx;
// Special path for multi-select drag: handle items from multiple tracks
if (drag.multiIds) {
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
var delta = newStart - dragOrigStart;
// Extend activeTracks if target track index exceeds track count
var outTracks = [];
Object.keys(drag.multiIds).forEach(function(mid) {
var inf = drag.multiIds[mid];
var oIdx = allTrks.findIndex(function(tr) { return tr.id === inf.trackId; });
if (oIdx < 0) oIdx = baseIdx;
var needIdx = oIdx + crossOffset;
if (needIdx >= 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 });
}
}
}
if (tracksToAdd.length > 0) prev = prev.concat(tracksToAdd);
return prev.map(function(t) {
});
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;
// Use ORIGINAL trackId stored at drag start for track offset math
var origTid = inf.trackId;
var origIdx = trkIds.indexOf(origTid);
if (origIdx < 0) origIdx = baseIdx;
var itemTrgIdx = Math.max(0, origIdx + crossOffset);
var itemTid = itemTrgIdx < prev.length ? prev[itemTrgIdx].id : null;
if (!itemTid) return;
// Remove from source if leaving for another track
var isOnThisTrack = false;
if (inf.type === 'section') isOnThisTrack = (t.sections || []).some(function(s) { return s.id === mid; });
else if (inf.type === 'midiItem') isOnThisTrack = (t.midiItems || []).some(function(mx) { return mx.id === mid; });
else if (inf.type === 'clip') isOnThisTrack = (t.clips || []).some(function(cx) { return cx.id === mid || 'default_' + t.id === mid; });
if (isOnThisTrack && t.id !== itemTid) {
if (inf.type === 'section') resS = resS.filter(function(s) { return s.id !== mid; });
else if (inf.type === 'midiItem') resM = resM.filter(function(mx) { return mx.id !== mid; });
else if (inf.type === 'clip') { var c3 = 'default_' + t.id; resC = resC.filter(function(cx) { return cx.id !== mid && cx.id !== c3; }); }
}
if (t.id === itemTid) {
var srcItem = null;
for (var pi2 = 0; pi2 < prev.length; pi2++) {
var tr2 = prev[pi2];
if (inf.type === 'section') srcItem = (tr2.sections || []).find(function(s) { return s.id === mid; });
else if (inf.type === 'midiItem') srcItem = (tr2.midiItems || []).find(function(mx) { return mx.id === mid; });
else if (inf.type === 'clip') { var c2 = (tr2.clips || []).find(function(cx) { return cx.id === mid || 'default_' + tr2.id === mid; }); if (c2) srcItem = c2; }
if (srcItem) break;
var oIdx = allIds.indexOf(inf.trackId);
if (oIdx < 0) oIdx = baseIdx;
var needIdx = Math.max(0, oIdx + crossOffset);
var targetTid = needIdx < merged.length ? merged[needIdx].id : merged[merged.length - 1].id;
if (inf.type === 'section') {
resS = resS.filter(function(s) { return s.id !== mid; });
if (t.id === targetTid) {
var src = null;
for (var p = 0; p < prev.length; p++) { src = (prev[p].sections || []).find(function(s) { return s.id === mid; }); if (src) break; }
if (src) resS.push({ ...src, start: Math.max(0, newVal) });
}
if (srcItem) {
if (inf.type === 'section') {
resS = resS.filter(function(s) { return s.id !== mid; });
resS.push({ ...srcItem, start: Math.max(0, newVal) });
} else if (inf.type === 'midiItem') {
resM = resM.filter(function(mx) { return mx.id !== mid; });
resM.push({ ...srcItem, startTime: Math.max(0, newVal) });
} else if (inf.type === 'clip') {
var c4 = 'default_' + t.id;
resC = resC.filter(function(cx) { return cx.id !== mid && cx.id !== c4; });
resC.push({ ...srcItem, startTime: Math.max(0, newVal) });
}
} else if (inf.type === 'midiItem') {
resM = resM.filter(function(mx) { return mx.id !== mid; });
if (t.id === targetTid) {
var src = null;
for (var p = 0; p < prev.length; p++) { src = (prev[p].midiItems || []).find(function(mx) { return mx.id === mid; }); if (src) break; }
if (src) resM.push({ ...src, startTime: Math.max(0, newVal) });
}
} else if (inf.type === 'clip') {
var cid3 = 'default_' + t.id;
resC = resC.filter(function(cx) { return cx.id !== mid && cx.id !== cid3; });
if (t.id === targetTid) {
var src = null;
for (var p = 0; p < prev.length; p++) { var c2 = (prev[p].clips || []).find(function(cx) { return cx.id === mid || 'default_' + prev[p].id === mid; }); if (c2) { src = c2; break; } }
if (src) resC.push({ ...src, startTime: Math.max(0, newVal) });
}
}
});
return { ...t, sections: resS, midiItems: resM, clips: resC };
});
}
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 };
});
});
} else {
// Single item drag path
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 = () => {