(() => { const shapeList = document.getElementById('shape-list'); const btnAdd = document.getElementById('btn-add-shape'); const btnRestore = document.getElementById('btn-restore-shapes'); const editor = document.getElementById('shape-editor'); const nameInput = document.getElementById('shape-name-input'); const fretInputs = document.getElementById('shape-fret-inputs'); const btnEditorSave = document.getElementById('btn-shape-save'); const btnEditorCancel = document.getElementById('btn-shape-cancel'); const qualitySelect = document.getElementById('shape-quality-select'); const rootSelect = document.getElementById('shape-root-select'); const voicingRow = document.getElementById('voicing-row'); const btnSearch = document.getElementById('btn-shape-search'); const resultsContainer = document.getElementById('shapes-results'); const loadingEl = document.getElementById('shapes-loading'); const setSelect = document.getElementById('score-set-select'); const roots = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; let scoreSetsData = { sets: [], selected: 0 }; let shapes = []; let selectedIndex = 0; let editingIndex = -1; function activeSet() { return scoreSetsData.sets[scoreSetsData.selected]; } function syncShapesRef() { const set = activeSet(); shapes = set ? set.shapes : []; } function init() { if (!window.go || !window.go.main || !window.go.main.App) return; window.go.main.App.GetScoreSets().then(data => { scoreSetsData = data; if (!scoreSetsData.sets || scoreSetsData.sets.length === 0) { scoreSetsData = { sets: [{ name: 'Standard Shapes', type: '', shapes: [] }], selected: 0 }; window.go.main.App.GetDefaultShapes().then(defaults => { scoreSetsData.sets[0].shapes = defaults; syncShapesRef(); renderSetSelector(); renderShapeList(); buildVoicingInputs(); }); return; } syncShapesRef(); selectedIndex = 0; renderSetSelector(); renderShapeList(); buildVoicingInputs(); }); roots.forEach((r, i) => { const opt = document.createElement('option'); opt.value = i; opt.textContent = r; rootSelect.appendChild(opt); }); window.go.main.App.GetChordDefinitions().then(defs => { const qualities = new Set(); for (const cat of Object.keys(defs)) { for (const q of Object.keys(defs[cat])) { qualities.add(q); } } Array.from(qualities).sort().forEach(q => { const opt = document.createElement('option'); opt.value = q; opt.textContent = q; qualitySelect.appendChild(opt); }); }); btnAdd.addEventListener('click', () => openEditor(-1)); btnRestore.addEventListener('click', restoreDefaults); btnEditorSave.addEventListener('click', saveEditor); btnEditorCancel.addEventListener('click', closeEditor); btnSearch.addEventListener('click', doSearch); setSelect.addEventListener('change', () => { if (window.dbg) window.dbg('[shapes] setSelect change:', setSelect.value); if (setSelect.value === '__editor__') { setSelect.value = scoreSetsData.selected; if (window.dbg) window.dbg('[shapes] opening editor, sets:', scoreSetsData.sets.length); window.switchToView('sets-editor'); if (window.initSetsEditor) window.initSetsEditor(); return; } switchSet(parseInt(setSelect.value)); }); resultsContainer.addEventListener('click', (e) => { const item = e.target.closest('[data-chord-midi]'); if (!item || !window.playChord) return; window.playChord(JSON.parse(item.dataset.chordMidi)); }); } function isDensitySet() { const set = activeSet(); return set && set.type === 'density'; } function renderSetSelector() { setSelect.innerHTML = ''; scoreSetsData.sets.forEach((set, i) => { const opt = document.createElement('option'); opt.value = i; opt.textContent = set.name; if (i === scoreSetsData.selected) opt.selected = true; setSelect.appendChild(opt); }); const sep = document.createElement('option'); sep.disabled = true; sep.textContent = '───'; setSelect.appendChild(sep); const editorOpt = document.createElement('option'); editorOpt.value = '__editor__'; editorOpt.textContent = 'Open Sets Editor'; setSelect.appendChild(editorOpt); } function switchSet(i) { if (i < 0 || i >= scoreSetsData.sets.length) return; scoreSetsData.selected = i; syncShapesRef(); selectedIndex = 0; closeEditor(); renderSetSelector(); renderShapeList(); buildVoicingInputs(); persistSets(); } function persistSets() { if (!window.go) return; const set = activeSet(); if (set) set.shapes = shapes; window.go.main.App.SaveScoreSets(scoreSetsData); } function renderShapeList() { shapeList.innerHTML = ''; shapes.forEach((s, i) => { const item = document.createElement('div'); item.className = 'shape-item' + (i === selectedIndex ? ' selected' : ''); const name = document.createElement('span'); name.className = 'shape-name'; name.textContent = s.name; const frets = document.createElement('span'); frets.className = 'shape-frets'; frets.textContent = s.frets.map(f => f === -1 ? 'x' : f).join(' '); const actions = document.createElement('span'); actions.className = 'shape-actions'; if (i > 0) { const upBtn = document.createElement('button'); upBtn.textContent = '\u25B2'; upBtn.title = 'Move up'; upBtn.addEventListener('click', e => { e.stopPropagation(); [shapes[i - 1], shapes[i]] = [shapes[i], shapes[i - 1]]; if (selectedIndex === i) selectedIndex = i - 1; else if (selectedIndex === i - 1) selectedIndex = i; renderShapeList(); buildVoicingInputs(); persistSets(); }); actions.appendChild(upBtn); } if (i < shapes.length - 1) { const downBtn = document.createElement('button'); downBtn.textContent = '\u25BC'; downBtn.title = 'Move down'; downBtn.addEventListener('click', e => { e.stopPropagation(); [shapes[i], shapes[i + 1]] = [shapes[i + 1], shapes[i]]; if (selectedIndex === i) selectedIndex = i + 1; else if (selectedIndex === i + 1) selectedIndex = i; renderShapeList(); buildVoicingInputs(); persistSets(); }); actions.appendChild(downBtn); } const editBtn = document.createElement('button'); editBtn.textContent = 'edit'; editBtn.addEventListener('click', e => { e.stopPropagation(); openEditor(i); }); const delBtn = document.createElement('button'); delBtn.textContent = 'del'; delBtn.addEventListener('click', e => { e.stopPropagation(); shapes.splice(i, 1); if (selectedIndex >= shapes.length) selectedIndex = Math.max(0, shapes.length - 1); renderShapeList(); buildVoicingInputs(); persistSets(); }); actions.appendChild(editBtn); actions.appendChild(delBtn); item.appendChild(name); item.appendChild(frets); item.appendChild(actions); item.addEventListener('click', () => { selectedIndex = i; renderShapeList(); buildVoicingInputs(); }); shapeList.appendChild(item); }); } function buildVoicingInputs() { voicingRow.innerHTML = ''; const shape = shapes[selectedIndex]; if (!shape) return; const labels = ['1','2','3','4','5','6']; for (let i = 0; i < 6; i++) { const inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = labels[i]; inp.dataset.string = i; if (shape.frets[i] === -1) { inp.disabled = true; inp.value = ''; inp.placeholder = 'x'; } voicingRow.appendChild(inp); } } function openEditor(idx) { editingIndex = idx; editor.style.display = ''; fretInputs.innerHTML = ''; if (idx >= 0) { nameInput.value = shapes[idx].name; for (let i = 0; i < 6; i++) { const inp = document.createElement('input'); inp.type = 'text'; inp.value = shapes[idx].frets[i] === -1 ? 'x' : shapes[idx].frets[i]; fretInputs.appendChild(inp); } } else { nameInput.value = ''; for (let i = 0; i < 6; i++) { const inp = document.createElement('input'); inp.type = 'text'; inp.value = '0'; fretInputs.appendChild(inp); } } } function closeEditor() { editor.style.display = 'none'; editingIndex = -1; } function saveEditor() { const name = nameInput.value.trim(); if (!name) return; const inputs = fretInputs.querySelectorAll('input'); const frets = []; for (const inp of inputs) { const v = inp.value.trim().toLowerCase(); if (v === 'x' || v === '-1') { frets.push(-1); } else { const n = parseInt(v); if (isNaN(n) || n < 0) { frets.push(0); } else { frets.push(n); } } } const shape = { name: name, frets: frets }; if (editingIndex >= 0) { shapes[editingIndex] = shape; } else { shapes.push(shape); selectedIndex = shapes.length - 1; } closeEditor(); renderShapeList(); buildVoicingInputs(); persistSets(); } function restoreDefaults() { if (!window.go) return; window.go.main.App.GetDefaultShapes().then(defaults => { shapes.length = 0; defaults.forEach(s => shapes.push(s)); const set = activeSet(); if (set) set.shapes = shapes; selectedIndex = 0; renderShapeList(); buildVoicingInputs(); persistSets(); }); } function doSearch() { if (!window.go) return; const shape = shapes[selectedIndex]; if (!shape) return; const voicingInputs = voicingRow.querySelectorAll('input'); const voicing = []; voicingInputs.forEach(inp => { voicing.push(inp.disabled ? '' : inp.value.trim()); }); const query = { shape: shape, target_quality: qualitySelect.value, target_root: parseInt(rootSelect.value), voicing: voicing, base_tuning: window.currentTuningMIDI ? window.currentTuningMIDI.slice() : [], baseline_shift: parseInt(document.getElementById('baseline-shift').value) || 0, range_down: parseInt(document.getElementById('range-down').value) || 0, range_up: parseInt(document.getElementById('range-up').value) || 0, }; loadingEl.style.display = ''; loadingEl.textContent = 'Searching tunings...'; resultsContainer.innerHTML = ''; if (isDensitySet()) { window.go.main.App.FindDensityTunings(query).then(results => { loadingEl.style.display = 'none'; renderDensityResults(results || []); }).catch(err => { loadingEl.style.display = 'none'; loadingEl.style.display = ''; loadingEl.textContent = 'Error: ' + err; }); } else { window.go.main.App.FindShapeTunings(query, shapes).then(results => { loadingEl.style.display = 'none'; renderResults(results || [], shape.name); }).catch(err => { loadingEl.style.display = 'none'; loadingEl.style.display = ''; loadingEl.textContent = 'Error: ' + err; }); } } const PAGE_SIZE = 10; function renderResults(results, searchShapeName) { resultsContainer.innerHTML = ''; if (results.length === 0) { const msg = document.createElement('div'); msg.className = 'loading'; msg.textContent = 'No tunings found.'; resultsContainer.appendChild(msg); return; } const compat = results.filter(r => r.high_compat).length; const summary = document.createElement('div'); summary.style.cssText = 'font-size:0.8rem;color:#888;margin-bottom:0.75rem;'; summary.textContent = results.length.toLocaleString() + ' possible tuning' + (results.length !== 1 ? 's' : '') + ' found' + (compat ? ' \u2014 ' + compat.toLocaleString() + ' highly compatible' : ''); resultsContainer.appendChild(summary); const listEl = document.createElement('div'); resultsContainer.appendChild(listEl); let shown = 0; let loadMoreBtn = null; function renderPage() { const end = Math.min(shown + PAGE_SIZE, results.length); for (let idx = shown; idx < end; idx++) { listEl.appendChild(buildTuningCard(results[idx], searchShapeName)); } shown = end; if (shown < results.length) { if (!loadMoreBtn) { loadMoreBtn = document.createElement('button'); loadMoreBtn.className = 'btn-load-more'; loadMoreBtn.addEventListener('click', renderPage); resultsContainer.appendChild(loadMoreBtn); } loadMoreBtn.textContent = 'Load more (' + (results.length - shown) + ' remaining)'; } else if (loadMoreBtn) { loadMoreBtn.remove(); loadMoreBtn = null; } } renderPage(); } function buildTuningCard(tc, searchShapeName) { const card = document.createElement('div'); card.className = 'tuning-card'; const header = document.createElement('div'); header.className = 'tuning-card-header'; const h3 = document.createElement('h3'); h3.textContent = tc.chord; if (tc.high_compat) { card.classList.add('high-compat'); const star = document.createElement('span'); star.className = 'compat-star'; star.textContent = '\u2605'; star.title = 'Highly compatible: ' + tc.maj_min_count + ' major/minor triads'; h3.appendChild(star); } const stats = document.createElement('span'); stats.className = 'tuning-stats'; stats.textContent = tc.valid_chords + '/' + (tc.companions ? tc.companions.length : 0) + ' chords'; const notes = document.createElement('span'); notes.className = 'tuning-notes'; notes.textContent = tc.tuning.join(' '); const arrow = document.createElement('span'); arrow.className = 'expand-icon'; arrow.textContent = '\u25B6'; header.appendChild(h3); header.appendChild(stats); header.appendChild(notes); header.appendChild(arrow); header.addEventListener('click', () => { card.classList.toggle('expanded'); }); const body = document.createElement('div'); body.className = 'tuning-card-body'; if (tc.companions && tc.companions.length > 0) { const grid = document.createElement('div'); grid.className = 'companion-grid'; tc.companions.forEach(comp => { const item = document.createElement('div'); item.className = 'companion-item'; const label = document.createElement('div'); label.className = 'companion-label'; label.textContent = comp.shape; if (comp.shape === searchShapeName) { label.style.color = '#1dc9fe'; } const chord = document.createElement('div'); chord.className = 'companion-chord'; chord.textContent = comp.chord; item.appendChild(label); item.appendChild(chord); const shapeDef = shapes.find(s => s.name === comp.shape); if (shapeDef) { const fingering = shapeDef.frets.map(f => f === -1 ? 'x' : String(f)); const maxFret = Math.max(...shapeDef.frets.filter(f => f >= 0), 1); const fb = document.createElement('div'); fb.className = 'fretboard alternative-fretboard'; renderSingleFretboard(fb, fingering, Math.max(maxFret, 4)); item.appendChild(fb); const midi = []; for (let si = 0; si < 6; si++) { if (shapeDef.frets[si] !== -1 && tc.tuning_midi && tc.tuning_midi[si] != null) { midi.push(tc.tuning_midi[si] + shapeDef.frets[si]); } } if (midi.length) item.dataset.chordMidi = JSON.stringify(midi); } const notesDiv = document.createElement('div'); notesDiv.className = 'companion-notes'; notesDiv.textContent = (comp.notes || []).join(' '); item.appendChild(notesDiv); grid.appendChild(item); }); body.appendChild(grid); } const setBtn = document.createElement('button'); setBtn.className = 'btn-small'; setBtn.textContent = 'Set to instrument tuning'; setBtn.style.marginTop = '0.75rem'; setBtn.addEventListener('click', () => { if (window.setTuningFromExplorer) window.setTuningFromExplorer(tc.tuning); }); body.appendChild(setBtn); card.appendChild(header); card.appendChild(body); return card; } function renderDensityResults(results) { resultsContainer.innerHTML = ''; if (results.length === 0) { const msg = document.createElement('div'); msg.className = 'loading'; msg.textContent = 'No tunings found.'; resultsContainer.appendChild(msg); return; } const explain = document.createElement('p'); explain.className = 'density-explanation'; explain.textContent = 'Tunings ranked by total playable chords within current fret/finger settings. Expand a card to see all identified chords with their simplest fingerings.'; resultsContainer.appendChild(explain); const compat = results.filter(r => r.high_compat).length; const summary = document.createElement('div'); summary.style.cssText = 'font-size:0.8rem;color:#888;margin-bottom:0.75rem;'; summary.textContent = results.length.toLocaleString() + ' tuning' + (results.length !== 1 ? 's' : '') + ' found' + (compat ? ' \u2014 ' + compat.toLocaleString() + ' highly compatible' : ''); resultsContainer.appendChild(summary); const listEl = document.createElement('div'); resultsContainer.appendChild(listEl); let shown = 0; let loadMoreBtn = null; function renderPage() { const end = Math.min(shown + PAGE_SIZE, results.length); for (let idx = shown; idx < end; idx++) { listEl.appendChild(buildDensityCard(results[idx])); } shown = end; if (shown < results.length) { if (!loadMoreBtn) { loadMoreBtn = document.createElement('button'); loadMoreBtn.className = 'btn-load-more'; loadMoreBtn.addEventListener('click', renderPage); resultsContainer.appendChild(loadMoreBtn); } loadMoreBtn.textContent = 'Load more (' + (results.length - shown) + ' remaining)'; } else if (loadMoreBtn) { loadMoreBtn.remove(); loadMoreBtn = null; } } renderPage(); } function buildDensityCard(dc) { const card = document.createElement('div'); card.className = 'tuning-card'; const header = document.createElement('div'); header.className = 'tuning-card-header'; const h3 = document.createElement('h3'); h3.textContent = dc.chord; if (dc.high_compat) { card.classList.add('high-compat'); const star = document.createElement('span'); star.className = 'compat-star'; star.textContent = '\u2605'; star.title = dc.maj_min_count + ' major/minor triads'; h3.appendChild(star); } const stats = document.createElement('span'); stats.className = 'tuning-stats'; stats.textContent = dc.valid_chords + ' chords, avg ' + dc.avg_fingers.toFixed(1) + ' fingers'; const notes = document.createElement('span'); notes.className = 'tuning-notes'; notes.textContent = dc.tuning.join(' '); const arrow = document.createElement('span'); arrow.className = 'expand-icon'; arrow.textContent = '\u25B6'; header.appendChild(h3); header.appendChild(stats); header.appendChild(notes); header.appendChild(arrow); header.addEventListener('click', () => { card.classList.toggle('expanded'); }); const body = document.createElement('div'); body.className = 'tuning-card-body'; if (dc.chords && dc.chords.length > 0) { const grid = document.createElement('div'); grid.className = 'companion-grid'; dc.chords.forEach(ch => { const item = document.createElement('div'); item.className = 'companion-item'; const chordLabel = document.createElement('div'); chordLabel.className = 'companion-chord'; chordLabel.textContent = ch.chord; const fingerBadge = document.createElement('span'); fingerBadge.className = 'finger-count'; fingerBadge.textContent = ch.fingers + 'f'; chordLabel.appendChild(fingerBadge); item.appendChild(chordLabel); const fingering = ch.frets.map(f => f === -1 ? 'x' : String(f)); const maxFretVal = Math.max(...ch.frets.filter(f => f >= 0), 1); const fb = document.createElement('div'); fb.className = 'fretboard alternative-fretboard'; renderSingleFretboard(fb, fingering, Math.max(maxFretVal, 4)); item.appendChild(fb); const midi = []; for (let si = 0; si < dc.tuning_midi.length; si++) { if (ch.frets[si] !== -1 && dc.tuning_midi[si] != null) { midi.push(dc.tuning_midi[si] + ch.frets[si]); } } if (midi.length) item.dataset.chordMidi = JSON.stringify(midi); const notesDiv = document.createElement('div'); notesDiv.className = 'companion-notes'; notesDiv.textContent = (ch.notes || []).join(' '); item.appendChild(notesDiv); grid.appendChild(item); }); body.appendChild(grid); } const setBtn = document.createElement('button'); setBtn.className = 'btn-small'; setBtn.textContent = 'Set to instrument tuning'; setBtn.style.marginTop = '0.75rem'; setBtn.addEventListener('click', () => { if (window.setTuningFromExplorer) window.setTuningFromExplorer(dc.tuning); }); body.appendChild(setBtn); card.appendChild(header); card.appendChild(body); return card; } function renderSingleFretboard(fb, fingering, maxFret) { const numStrings = fingering.length; fb.innerHTML = ''; fb.style.display = 'inline-block'; const fretCounts = {}; fingering.forEach(f => { if (!isNaN(f)) fretCounts[f] = (fretCounts[f] || 0) + 1; }); const entries = Object.entries(fretCounts) .filter(([, count]) => count >= 2) .map(([f]) => parseInt(f)); let barreFretNum = null; for (const f of entries.sort((a, b) => a - b)) { if (fingering.every(x => x === 'x' || isNaN(x) || parseInt(x) >= f)) { barreFretNum = f; break; } } const fretMatrix = []; for (let s = 0; s < numStrings; s++) { const stringRow = []; for (let f = 1; f <= maxFret; f++) { const fret = document.createElement('div'); fret.className = 'fret'; const fretValue = fingering[s]; const numericFret = parseInt(fretValue, 10); if (fretValue === 'x' && f === 1) { fret.setAttribute('muted', ''); fret.textContent = 'x'; } else if (fretValue !== 'x' && numericFret === f) { fret.dataset.dot = 'true'; if (barreFretNum !== null && numericFret === barreFretNum) { fret.classList.add('barre'); } } stringRow.push(fret); } fretMatrix.push(stringRow); } const wrapper = document.createElement('div'); wrapper.className = 'fretboard'; for (let s = numStrings - 1; s >= 0; s--) { const row = document.createElement('div'); row.className = 'fret-row'; for (let f = 1; f <= maxFret; f++) { row.appendChild(fretMatrix[s][f - 1]); } wrapper.appendChild(row); } fb.appendChild(wrapper); if (barreFretNum !== null) { const barreCols = []; for (let s = 0; s < numStrings; s++) { if (parseInt(fingering[s]) === barreFretNum) barreCols.push(s); } if (barreCols.length >= 2) { const start = Math.min(...barreCols); const end = Math.max(...barreCols); const line = document.createElement('div'); line.className = 'barre-line'; requestAnimationFrame(() => { let totalDotCenter = 0; let dotCount = 0; for (let s = start; s <= end; s++) { const dotFret = fretMatrix[s][barreFretNum - 1]; if (dotFret) { const rect = dotFret.getBoundingClientRect(); totalDotCenter += (rect.left + rect.right) / 2; dotCount++; } } if (!dotCount) return; const avgDotCenter = totalDotCenter / dotCount; const parentRect = wrapper.getBoundingClientRect(); const dotCenter = avgDotCenter - parentRect.left; const firstDot = fretMatrix[start][barreFretNum - 1]; const lastDot = fretMatrix[end][barreFretNum - 1]; const rect1 = firstDot.getBoundingClientRect(); const rect2 = lastDot.getBoundingClientRect(); const top = Math.min(rect1.top, rect2.top) - parentRect.top; const bottom = Math.max(rect1.bottom, rect2.bottom) - parentRect.top; const height = bottom - top; line.style.top = Math.round(top) + 'px'; line.style.height = Math.round(height) + 'px'; line.style.left = Math.round(dotCenter) + 'px'; line.textContent = '|'; wrapper.appendChild(line); }); } } } window.initShapeExplorer = init; window.renderSingleFretboard = renderSingleFretboard; window.getScoreSetsData = () => scoreSetsData; window.setScoreSetsData = (data) => { scoreSetsData = data; syncShapesRef(); renderSetSelector(); renderShapeList(); buildVoicingInputs(); }; window.persistSets = persistSets; })();