fix: group drag cross-track maintains relative track offsets

Each item computes its own target track as srcIdx + crossOffset.
Items from different source tracks land on different target tracks,
preserving the group's vertical layout shape.
This commit is contained in:
2026-07-29 10:52:50 +07:00
parent 12d2639455
commit 9c3a435f26
3 changed files with 66 additions and 12 deletions
+54 -11
View File
@@ -12049,6 +12049,16 @@ const App = () => {
wrapper.scrollLeft = Math.max(0, itemPx - keepMargin);
setCanvasRedrawCount(n => n + 1);
}
var allTrks = activeTracksRef.current || [];
var targetTrackId = hoveredTrackIdRef.current || drag.trackId;
// Build trackId->index map for relative offset computation
var trkIndexMap = {};
allTrks.forEach(function(tr, ti) { trkIndexMap[tr.id] = ti; });
var baseIdx = trkIndexMap[drag.trackId] || 0;
var targetIdx = trkIndexMap[targetTrackId];
if (targetIdx === undefined) { targetIdx = allTrks.length - 1; if (targetIdx < 0) targetIdx = 0; }
var crossOffset = targetIdx - baseIdx;
// Build live currentTrackMap: where each multi-drag item currently lives
updateActiveTracks(prev => {
let movedItem = null;
for (let track of prev) {
@@ -12057,6 +12067,19 @@ const App = () => {
if (sec || mid) { movedItem = sec || mid; break; }
}
if (drag.multiIds) {
// Build currentTrackMap from live state
var currentTrackMap = {};
Object.keys(drag.multiIds).forEach(function(mid) {
for (var pi = 0; pi < prev.length; pi++) {
var pt = prev[pi];
var f = false;
var inf = drag.multiIds[mid];
if (inf.type === 'section') f = (pt.sections || []).some(function(s) { return s.id === mid; });
else if (inf.type === 'midiItem') f = (pt.midiItems || []).some(function(mx) { return mx.id === mid; });
else if (inf.type === 'clip') f = (pt.clips || []).some(function(cx) { return cx.id === mid || 'default_' + pt.id === mid; });
if (f) { currentTrackMap[mid] = pt.id; break; }
}
});
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
var delta = newStart - dragOrigStart;
return prev.map(t => {
@@ -12064,17 +12087,34 @@ const App = () => {
var resultMidis = (t.midiItems || []).slice();
var resultClips = (t.clips || []).slice();
Object.keys(drag.multiIds).forEach(function(mid) {
var info = drag.multiIds[mid];
var newVal = info.start + delta;
if (info.type === 'section') {
var idx = resultSections.findIndex(function(s) { return s.id === mid; });
if (idx >= 0) resultSections[idx] = { ...resultSections[idx], start: Math.max(0, newVal) };
} else if (info.type === 'midiItem') {
var idx = resultMidis.findIndex(function(mx) { return mx.id === mid; });
if (idx >= 0) resultMidis[idx] = { ...resultMidis[idx], startTime: Math.max(0, newVal) };
} else if (info.type === 'clip') {
var idx = resultClips.findIndex(function(cx) { return cx.id === mid || 'default_' + t.id === mid; });
if (idx >= 0) resultClips[idx] = { ...resultClips[idx], startTime: Math.max(0, newVal) };
var inf = drag.multiIds[mid];
var newVal = inf.start + delta;
var srcTid = currentTrackMap[mid];
var srcIdx = trkIndexMap[srcTid] !== undefined ? trkIndexMap[srcTid] : baseIdx;
var itemTargetIdx = Math.max(0, srcIdx + crossOffset);
var itemTid = itemTargetIdx < allTrks.length ? allTrks[itemTargetIdx].id : null;
if (!itemTid) return;
if (srcTid === t.id && t.id !== itemTid) {
// Remove from source track (leaving for another track)
if (inf.type === 'section') resultSections = resultSections.filter(function(s) { return s.id !== mid; });
else if (inf.type === 'midiItem') resultMidis = resultMidis.filter(function(mx) { return mx.id !== mid; });
else if (inf.type === 'clip') { var cid3 = 'default_' + t.id; resultClips = resultClips.filter(function(cx) { return cx.id !== mid && cx.id !== cid3; }); }
}
if (t.id === itemTid) {
// This IS the item's target track upsert
if (inf.type === 'section') {
resultSections = resultSections.filter(function(s) { return s.id !== mid; });
resultSections.push({ ...(prev.flatMap(function(x) { return x.sections || []; }).find(function(s) { return s.id === mid; }) || {}), start: Math.max(0, newVal) });
} else if (inf.type === 'midiItem') {
resultMidis = resultMidis.filter(function(mx) { return mx.id !== mid; });
resultMidis.push({ ...(prev.flatMap(function(x) { return x.midiItems || []; }).find(function(mx) { return mx.id === mid; }) || {}), startTime: Math.max(0, newVal) });
} else if (inf.type === 'clip') {
var cid4 = 'default_' + t.id;
resultClips = resultClips.filter(function(cx) { return cx.id !== mid && cx.id !== cid4; });
var allClips = prev.flatMap(function(x) { return x.clips || []; });
var srcClip = allClips.find(function(cx) { return cx.id === mid || 'default_' + (srcTid || '') === mid; });
if (srcClip) resultClips.push({ ...srcClip, startTime: Math.max(0, newVal) });
}
}
});
return { ...t, sections: resultSections, midiItems: resultMidis, clips: resultClips };
@@ -12091,6 +12131,9 @@ const App = () => {
? { ...t, sections: updatedItems }
: { ...t, midiItems: updatedItems });
});
if (drag.trackId !== targetTrackId && typeof setDraggedSectionItem === 'function') {
setDraggedSectionItem(function(p) { return { ...p, trackId: targetTrackId }; });
}
if (drag.trackId !== targetTrackId) {
setDraggedSectionItem(prev => ({ ...prev, trackId: targetTrackId }));
}