diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js index e2c8864e..3897951d 100755 --- a/assets/js/creations/audio/audio_ui.js +++ b/assets/js/creations/audio/audio_ui.js @@ -187,6 +187,12 @@ export function renderAudioEditor() { const existingTrackContainer = document.getElementById("audio-track-container"); if (!audioEditor || !existingTrackContainer) return; + + // ✅ salva scroll atual para não “pular” pro início após render + const prevScrollLeft = + (existingTrackContainer.scrollLeft ?? 0) || (appState.audio.editorScrollLeft ?? 0); + const prevScrollTop = + (existingTrackContainer.scrollTop ?? 0) || (appState.audio.editorScrollTop ?? 0); _ensureGlobalPlaylistSelectionFields(); _installPlaylistKeybindOnce(); @@ -427,6 +433,9 @@ export function renderAudioEditor() { tracksParent.replaceChild(newTrackContainer, existingTrackContainer); // ✅ único scroller horizontal/vertical do editor const scrollEl = newTrackContainer; // #audio-track-container + // ✅ restaura scroll imediatamente após recriar o container + newTrackContainer.scrollLeft = prevScrollLeft; + newTrackContainer.scrollTop = prevScrollTop; // === RENDERIZAÇÃO DAS PISTAS (LANES) === @@ -604,6 +613,19 @@ export function renderAudioEditor() { clipDiv.appendChild(leftHandle); clipDiv.appendChild(rightHandle); + // ✅ garante área clicável dos handles mesmo sem CSS + [leftHandle, rightHandle].forEach((h) => { + h.style.position = "absolute"; + h.style.top = "0"; + h.style.bottom = "0"; + h.style.width = "8px"; + h.style.cursor = "ew-resize"; + h.style.zIndex = "8"; + h.style.background = "rgba(255,255,255,0.10)"; + }); + leftHandle.style.left = "0"; + rightHandle.style.right = "0"; + // ✅ overlay de “marquinhas pretas” (loop interno da pattern) const loopSteps = getLoopStepsForBasslineLane(trackData); const loopPx = loopSteps * stepWidthPx; @@ -777,6 +799,10 @@ export function renderAudioEditor() { newTrackContainer.addEventListener("scroll", () => { const scrollPos = scrollEl.scrollLeft; + // ✅ persiste scroll para sobreviver a qualquer render + appState.audio.editorScrollLeft = scrollPos; + appState.audio.editorScrollTop = scrollEl.scrollTop || 0; + // sincroniza régua com o container const mainRuler = tracksParent.querySelector(".timeline-ruler"); if (mainRuler && mainRuler.scrollLeft !== scrollPos) { @@ -817,6 +843,7 @@ export function renderAudioEditor() { const currentPixelsPerSecond = getPixelsPerSecond(); const handle = e.target.closest(".clip-resize-handle"); + const patternHandle = e.target.closest(".pattern-resize-handle"); // Slice Tool if (appState.global.sliceToolActive && clipElement && !clipElement.classList.contains("bassline-clip")) { @@ -843,6 +870,119 @@ export function renderAudioEditor() { return; } + // ========================================================= + // ✅ PATTERN (bassline) drag horizontal + resize L/R + // (sem perder duplo clique) + // ========================================================= + if (clipElement && clipElement.classList.contains("bassline-clip") && e.button === 0) { + // ✅ não inicia drag/resize no 2º clique do duplo clique + if (e.detail && e.detail > 1) return; + + e.preventDefault(); + + const patternIndex = Number(clipElement.dataset.patternIndex ?? 0); + const plClipId = String(clipElement.dataset.plClipId || ""); + if (!plClipId) return; + + _ensureGlobalPlaylistSelectionFields(); + + // seleção + appState.global.selectedPlaylistClipId = plClipId; + appState.global.selectedPlaylistPatternIndex = patternIndex; + + newTrackContainer + .querySelectorAll(".bassline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + clipElement.classList.add("selected"); + + const model = _getPlaylistClipModel(patternIndex, plClipId); + if (!model) return; + + const initialMouseX = e.clientX; + const initialScrollLeft = scrollEl.scrollLeft; + + const initialPosTicks = Number(model.pos || 0); + const initialLenTicks = Math.max(PL_MIN_LEN_TICKS, Number(model.len || 192)); + const initialEndTicks = initialPosTicks + initialLenTicks; + + const preview = (posTicks, lenTicks) => { + const leftPx = _ticksToPx(posTicks, stepWidthPx); + const widthPx = Math.max(1, _ticksToPx(lenTicks, stepWidthPx)); + clipElement.style.left = `${leftPx}px`; + clipElement.style.width = `${widthPx}px`; + clipElement.style.backgroundPosition = `-${leftPx}px 0px`; + }; + + const handleType = patternHandle + ? (patternHandle.classList.contains("left") ? "left" : "right") + : null; + + const onMouseMove = (moveEvent) => { + const deltaX = + (moveEvent.clientX - initialMouseX) + + (scrollEl.scrollLeft - initialScrollLeft); + + const deltaTicks = _pxToTicks(deltaX, stepWidthPx); + + // drag + if (!handleType) { + let newPos = _snapTicks(initialPosTicks + deltaTicks, PL_SNAP_TICKS); + newPos = Math.max(0, newPos); + preview(newPos, initialLenTicks); + return; + } + + // resize right + if (handleType === "right") { + let newEnd = _snapTicks(initialEndTicks + deltaTicks, PL_SNAP_TICKS); + newEnd = Math.max(initialPosTicks + PL_MIN_LEN_TICKS, newEnd); + preview(initialPosTicks, newEnd - initialPosTicks); + return; + } + + // resize left + let newPos = _snapTicks(initialPosTicks + deltaTicks, PL_SNAP_TICKS); + newPos = Math.max(0, newPos); + newPos = Math.min(newPos, initialEndTicks - PL_MIN_LEN_TICKS); + preview(newPos, initialEndTicks - newPos); + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + const finalLeftPx = clipElement.offsetLeft; + const finalWidthPx = clipElement.offsetWidth; + + let posTicks = _snapTicks(_pxToTicks(finalLeftPx, stepWidthPx), PL_SNAP_TICKS); + let lenTicks = _snapTicks(_pxToTicks(finalWidthPx, stepWidthPx), PL_SNAP_TICKS); + + posTicks = Math.max(0, posTicks); + lenTicks = Math.max(PL_MIN_LEN_TICKS, lenTicks); + + _updatePlaylistClipLocal(patternIndex, plClipId, { pos: posTicks, len: lenTicks }); + + sendActionSafe({ + type: "UPDATE_PLAYLIST_PATTERN_CLIP", + patternIndex, + clipId: plClipId, + pos: posTicks, + len: lenTicks, + }); + + // ✅ mantém scroll onde você estava antes do render + appState.audio.editorScrollLeft = scrollEl.scrollLeft; + appState.audio.editorScrollTop = scrollEl.scrollTop || 0; + + renderAudioEditor(); + restartAudioEditorIfPlaying(); + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; + } + // Resize Handle if (handle) { e.preventDefault(); @@ -1097,6 +1237,17 @@ export function renderAudioEditor() { } } }); + + // ✅ garante scroll mesmo após montar tudo (importante em DOM grande) + requestAnimationFrame(() => { + try { + newTrackContainer.scrollLeft = prevScrollLeft; + newTrackContainer.scrollTop = prevScrollTop; + + const mainRuler = tracksParent.querySelector(".timeline-ruler"); + if (mainRuler) mainRuler.scrollLeft = prevScrollLeft; + } catch {} + }); } export function updateAudioEditorUI() {