manipulando patterns na playlist
Deploy / Deploy (push) Successful in 2m2s Details

This commit is contained in:
JotaChina 2025-12-27 14:30:30 -03:00
parent f7c81dd1de
commit 8b811aee07
1 changed files with 151 additions and 0 deletions

View File

@ -188,6 +188,12 @@ export function renderAudioEditor() {
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() {