diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index 0f23c0fd..9f645b6c 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -544,31 +544,31 @@ export async function parseMmpContent(xmlString) { const basslineContainers = bbTrackNodes .map((trackNode, idx) => { const trackName = trackNode.getAttribute("name") || "Beat/Bassline"; - - const playlistClips = Array.from( - trackNode.querySelectorAll(":scope > bbtco") - ).map((bbtco) => ({ - pos: parseInt(bbtco.getAttribute("pos"), 10) || 0, - len: parseInt(bbtco.getAttribute("len"), 10) || 192, - name: trackName, - })); - - // ❌ remove esta linha: - // if (playlistClips.length === 0) return null; - - // ✅ mantém mesmo vazia + const playlistClips = Array.from(trackNode.querySelectorAll(":scope > bbtco")).map((bbtco, cidx) => { + const pos = parseInt(bbtco.getAttribute("pos"), 10) || 0; + const len = parseInt(bbtco.getAttribute("len"), 10) || 192; return { - id: `bassline_${Date.now()}_${Math.random().toString(36).slice(2)}`, + id: `plc_${idx}_${pos}_${len}_${cidx}`, // determinístico + pos, + len, name: trackName, - type: "bassline", - playlist_clips: playlistClips, // pode ser [] - patternIndex: idx, - instrumentSourceId: rackId, - volume: 1, - pan: 0, - patterns: [], - isMuted: trackNode.getAttribute("muted") === "1", }; + }); + + // NÃO retornar null quando não tem clips: + return { + id: `bb_container_${idx}`, + name: trackName, + type: "bassline", + patternIndex: idx, + playlist_clips: playlistClips, // pode ser [] + patterns: [], + isMuted: false, + instrumentSourceId: bbRackId, // ou algo equivalente no teu parse + volume: 1, + pan: 0, + }; + }) .filter(Boolean); diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 472b9a37..0e5c1d58 100755 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -213,8 +213,27 @@ document.addEventListener("DOMContentLoaded", () => { } }); - //Seleção de pattern + // --- Criar/Remover Pattern (toolbar) --- + const addPatternBtn = document.getElementById("add-pattern-btn"); + const removePatternBtn = document.getElementById("remove-pattern-btn"); + addPatternBtn?.addEventListener("click", () => { + const refTrack = (appState.pattern.tracks || []).find(t => t.type !== "bassline"); + const nextIndex = refTrack?.patterns?.length ?? 0; + + const defaultName = `Pattern ${nextIndex + 1}`; + const name = (prompt("Nome do novo pattern:", defaultName) || defaultName).trim(); + + // cria e já seleciona + sendAction({ type: "ADD_PATTERN", patternIndex: nextIndex, name, select: true }); + }); + + // opcional (se quiser implementar remover depois) + removePatternBtn?.addEventListener("click", () => { + sendAction({ type: "REMOVE_LAST_PATTERN" }); + }); + + //Seleção de pattern const globalPatternSelector = document.getElementById( "global-pattern-selector" ); diff --git a/assets/js/creations/socket.js b/assets/js/creations/socket.js index 3e7abf7b..27738305 100755 --- a/assets/js/creations/socket.js +++ b/assets/js/creations/socket.js @@ -499,11 +499,15 @@ function _ensureBasslineForPatternIndex(patternIndex) { instrumentSourceId: null, volume: 1, pan: 0, + instrumentSourceId: _getDefaultRackId(), }; appState.pattern.tracks.push(b); } if (!Array.isArray(b.playlist_clips)) b.playlist_clips = []; + + // Isso deixa o modo focado funcionando em patterns recém-criados. + if (!b.instrumentSourceId) b.instrumentSourceId = _getDefaultRackId(); // garante ids nos clips antigos b.playlist_clips.forEach((c) => { @@ -513,6 +517,13 @@ function _ensureBasslineForPatternIndex(patternIndex) { return b; } +function _getDefaultRackId() { + const inst = (appState.pattern.tracks || []).find( + (t) => t.type !== "bassline" && t.parentBasslineId + ); + return inst?.parentBasslineId ?? null; +} + // ----------------------------------------------------------------------------- // BROADCAST // ----------------------------------------------------------------------------- @@ -708,6 +719,71 @@ async function handleActionBroadcast(action) { break; } + case "ADD_PATTERN": { + const who = actorOf(action); + + const desiredIndex = Number.isFinite(Number(action.patternIndex)) + ? Number(action.patternIndex) + : null; + + const nonBass = (appState.pattern.tracks || []).filter(t => t.type !== "bassline"); + const currentCount = nonBass.reduce((m, t) => Math.max(m, t.patterns?.length ?? 0), 0); + + const idx = desiredIndex != null ? desiredIndex : currentCount; + const finalName = String(action.name || `Pattern ${idx + 1}`).trim(); + + // 1) cria patterns vazios em TODAS as tracks (respeita bars-input) + _ensurePatternsUpTo(idx); + + // 2) garante nome/pos estáveis no índice criado + for (const t of nonBass) { + t.patterns[idx] = t.patterns[idx] || _makeEmptyPattern(idx); + t.patterns[idx].name = finalName; + if (t.patterns[idx].pos == null) t.patterns[idx].pos = idx * LMMS_BAR_TICKS; + } + + // 3) cria a lane "bassline" (a coluna do pattern) + const b = _ensureBasslineForPatternIndex(idx); + b.patternIndex = idx; + b.name = finalName; + if (!b.instrumentSourceId) b.instrumentSourceId = _getDefaultRackId(); + + // 4) opcional: já selecionar + if (action.select) { + appState.pattern.activePatternIndex = idx; + appState.pattern.tracks.forEach((track) => (track.activePatternIndex = idx)); + } + + renderAll(); + showToast(`➕ ${who} criou: ${finalName}`, "success"); + saveStateToSession(); + break; + } + + case "REMOVE_LAST_PATTERN": { + const nonBass = (appState.pattern.tracks || []).filter(t => t.type !== "bassline"); + const count = nonBass.reduce((m, t) => Math.max(m, t.patterns?.length ?? 0), 0); + const last = count - 1; + if (last <= 0) break; + + // remove patterns nas tracks + for (const t of nonBass) t.patterns.pop(); + + // remove a lane bassline correspondente + appState.pattern.tracks = appState.pattern.tracks.filter( + t => !(t.type === "bassline" && Number(t.patternIndex) === last) + ); + + // ajusta seleção + const newIdx = Math.min(appState.pattern.activePatternIndex, last - 1); + appState.pattern.activePatternIndex = newIdx; + appState.pattern.tracks.forEach((t) => (t.activePatternIndex = newIdx)); + + renderAll(); + saveStateToSession(); + break; + } + case "ADD_PLAYLIST_PATTERN_CLIP": { const { patternIndex, pos, len, clipId, name } = action;