// pg4-debug.js - Modo debug e calibração visual para pg4 // Renderiza o campo KDE real sobre a matriz + marcadores de picos + painel de sliders. // Independente de pg4.js: reimplementa o pipeline com parâmetros mutáveis. (function () { 'use strict'; // ========================================================================= // CONFIG INICIAL // ========================================================================= const DEBUG_CONFIG = { bandwidth: 10, massMin: 0.15, dominanceRatio: 0.50, prominenceMin: 0.25, maxPGs: 3, minSep: 18 }; const GRID_SIZE = 100; const DEBUG_CANVAS_SIZE = 490; const DEBUG_CANVAS_TOP = '43%'; const DEBUG_CANVAS_LEFT = '50%'; const DEBUG_CANVAS_TRANSFORM = 'translate(-50%, -50%)'; // ========================================================================= // ESTADO // ========================================================================= const debugState = { active: false, pontos: [], grid: null, maxGlobal: 0, picosMarcados: [], pGsFinais: [], canvas: null, panel: null }; // ========================================================================= // BOOTSTRAP // ========================================================================= console.log('pg4-debug: script carregado'); function init() { injectStyles(); tentarInjetarBotao(10); } function tentarInjetarBotao(tentativas) { if (document.getElementById('pg4DebugBtn')) return; const sucesso = injectButton(); if (!sucesso && tentativas > 0) { setTimeout(() => tentarInjetarBotao(tentativas - 1), 400); } } function injectButton() { const btn = document.createElement('button'); btn.className = 'button'; btn.id = 'pg4DebugBtn'; btn.title = 'Debug KDE (pg4)'; btn.innerHTML = ''; btn.onclick = toggleDebug; const inputArea = document.getElementById('inputArea'); if (inputArea) { const containers = inputArea.querySelectorAll('.button-container'); if (containers.length > 0) { const novo = document.createElement('div'); novo.className = 'button-container'; novo.appendChild(btn); const ultimo = containers[containers.length - 1]; ultimo.parentNode.insertBefore(novo, ultimo.nextSibling); console.log('pg4-debug: botão injetado no inputArea'); return true; } } btn.style.position = 'fixed'; btn.style.bottom = '20px'; btn.style.left = '20px'; btn.style.zIndex = '500'; btn.style.backgroundColor = 'rgba(30, 20, 70, 0.95)'; btn.style.borderColor = '#ff0066'; document.body.appendChild(btn); console.log('pg4-debug: botão injetado como fallback fixo'); return true; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // ========================================================================= // ABRIR / FECHAR // ========================================================================= function toggleDebug() { if (debugState.active) { fecharDebug(); } else { abrirDebug(); } } function abrirDebug() { const hidden = document.getElementById('hiddenCoordinates'); if (!hidden) { alert("pg4-debug: campo 'hiddenCoordinates' não encontrado."); return; } debugState.pontos = extrairPontos(hidden.value); if (debugState.pontos.length < 5) { alert('pg4-debug: pontos insuficientes para análise (mínimo 5).'); return; } criarCanvas(); criarPainel(); rodarAnalise(); debugState.active = true; } function fecharDebug() { const antigoCanvas = document.getElementById('pg4DebugCanvas'); const antigoPainel = document.getElementById('pg4DebugPanel'); if (antigoCanvas) antigoCanvas.remove(); if (antigoPainel) antigoPainel.remove(); debugState.canvas = null; debugState.panel = null; debugState.active = false; } function criarCanvas() { const matrix = document.getElementById('matrix'); if (!matrix) return; const antigo = document.getElementById('pg4DebugCanvas'); if (antigo) antigo.remove(); const canvas = document.createElement('canvas'); canvas.id = 'pg4DebugCanvas'; canvas.width = DEBUG_CANVAS_SIZE; canvas.height = DEBUG_CANVAS_SIZE; canvas.style.position = 'absolute'; canvas.style.top = DEBUG_CANVAS_TOP; canvas.style.left = DEBUG_CANVAS_LEFT; canvas.style.transform = DEBUG_CANVAS_TRANSFORM; canvas.style.pointerEvents = 'none'; canvas.style.zIndex = '50'; matrix.appendChild(canvas); debugState.canvas = canvas; } // ========================================================================= // PAINEL DE CONTROLES // ========================================================================= function criarPainel() { const antigo = document.getElementById('pg4DebugPanel'); if (antigo) antigo.remove(); const panel = document.createElement('div'); panel.id = 'pg4DebugPanel'; panel.innerHTML = `
pg4 · calibração KDE
${sliderHtml('bw', 'BANDWIDTH', 5, 20, 0.5, DEBUG_CONFIG.bandwidth, 'Largura do kernel gaussiano.')} ${sliderHtml('mm', 'MASS_MIN', 0.05, 0.40, 0.01, DEBUG_CONFIG.massMin, 'Massa mínima do pG primário.')} ${sliderHtml('dr', 'DOMINANCE', 0.20, 0.90, 0.05, DEBUG_CONFIG.dominanceRatio, 'Fração da massa do primário para secundários.')} ${sliderHtml('pm', 'PROMINENCE', 0.05, 0.60, 0.05, DEBUG_CONFIG.prominenceMin, 'Proeminência mínima do pico.')}
pG aprovado
candidato (filtrado)
pico sem proeminência
`; document.body.appendChild(panel); debugState.panel = panel; panel.querySelector('.pg4d-close').addEventListener('click', fecharDebug); panel.querySelector('#pg4d-copy').addEventListener('click', copiarConfig); ['bw', 'mm', 'dr', 'pm'].forEach(k => { const input = document.getElementById(`pg4d-${k}`); const val = document.getElementById(`pg4d-${k}-val`); input.addEventListener('input', () => { val.textContent = input.value; onParamChange(); }); }); } function sliderHtml(key, label, min, max, step, val, tip) { return ` `; } function onParamChange() { const bwNew = parseFloat(document.getElementById('pg4d-bw').value); const bwChanged = bwNew !== DEBUG_CONFIG.bandwidth; DEBUG_CONFIG.bandwidth = bwNew; DEBUG_CONFIG.massMin = parseFloat(document.getElementById('pg4d-mm').value); DEBUG_CONFIG.dominanceRatio = parseFloat(document.getElementById('pg4d-dr').value); DEBUG_CONFIG.prominenceMin = parseFloat(document.getElementById('pg4d-pm').value); if (bwChanged) { rodarAnalise(); } else { debugState.picosMarcados = enriquecer( detectarPicos(debugState.grid, DEBUG_CONFIG.bandwidth), debugState.pontos, debugState.grid, DEBUG_CONFIG.bandwidth ); const sel = selecionar(debugState.picosMarcados, DEBUG_CONFIG); debugState.picosMarcados = sel.marcados; debugState.pGsFinais = sel.selecionados; renderizar(); atualizarStats(); } } // ========================================================================= // PIPELINE // ========================================================================= function rodarAnalise() { debugState.grid = construirGrid(debugState.pontos, DEBUG_CONFIG.bandwidth); debugState.maxGlobal = maximoGlobal(debugState.grid); const picos = detectarPicos(debugState.grid, DEBUG_CONFIG.bandwidth); const enriquecidos = enriquecer(picos, debugState.pontos, debugState.grid, DEBUG_CONFIG.bandwidth); const sel = selecionar(enriquecidos, DEBUG_CONFIG); debugState.picosMarcados = sel.marcados; debugState.pGsFinais = sel.selecionados; renderizar(); atualizarStats(); } function extrairPontos(input) { return input.split('\n').map(linha => { if (/^[a-zA-Z]+[a-zA-Z0-9_-]*-/.test(linha)) return null; const pura = linha.split('>')[0].split('(r=')[0]; const partes = pura.split(','); if (partes.length === 4 && partes.every(p => !isNaN(parseFloat(p)))) { return { x: parseFloat(partes[3]), y: parseFloat(partes[1]) }; } return null; }).filter(p => p !== null); } function construirGrid(pontos, bw) { const grid = Array.from({ length: GRID_SIZE }, () => new Float64Array(GRID_SIZE)); const h2 = 2 * bw * bw; const raio = Math.ceil(bw * 3); pontos.forEach(p => { const xMin = Math.max(0, Math.floor(p.x - raio)); const xMax = Math.min(GRID_SIZE - 1, Math.ceil(p.x + raio)); const yMin = Math.max(0, Math.floor(p.y - raio)); const yMax = Math.min(GRID_SIZE - 1, Math.ceil(p.y + raio)); for (let i = xMin; i <= xMax; i++) { for (let j = yMin; j <= yMax; j++) { const dx = i - p.x; const dy = j - p.y; grid[i][j] += Math.exp(-(dx * dx + dy * dy) / h2); } } }); return grid; } function maximoGlobal(grid) { let max = 0; for (let i = 0; i < GRID_SIZE; i++) { for (let j = 0; j < GRID_SIZE; j++) { if (grid[i][j] > max) max = grid[i][j]; } } return max; } function detectarPicos(grid, bw) { const picos = []; const raio = Math.max(2, Math.floor(bw / 2)); const threshold = maximoGlobal(grid) * 0.05; for (let i = raio; i < GRID_SIZE - raio; i++) { for (let j = raio; j < GRID_SIZE - raio; j++) { const v = grid[i][j]; if (v < threshold) continue; let ehMax = true; for (let di = -raio; di <= raio && ehMax; di++) { for (let dj = -raio; dj <= raio && ehMax; dj++) { if (di === 0 && dj === 0) continue; if (grid[i + di][j + dj] > v) ehMax = false; } } if (ehMax) picos.push({ x: i, y: j, densidade: v }); } } return picos; } function enriquecer(picos, pontos, grid, bw) { const total = pontos.length; const raioMassa = bw * 1.5; return picos.map(pico => { const massa = pontos.filter(p => Math.sqrt((p.x - pico.x) ** 2 + (p.y - pico.y) ** 2) <= raioMassa ).length / total; const raio = Math.ceil(bw * 2); let vale = pico.densidade; for (let di = -raio; di <= raio; di++) { for (let dj = -raio; dj <= raio; dj++) { const i = pico.x + di; const j = pico.y + dj; if (i < 0 || i >= GRID_SIZE || j < 0 || j >= GRID_SIZE) continue; if (di * di + dj * dj > raio * raio) continue; if (grid[i][j] < vale) vale = grid[i][j]; } } const prom = (pico.densidade - vale) / pico.densidade; return { ...pico, massa, proeminencia: prom }; }); } function selecionar(picos, cfg) { const marcados = picos.map(p => ({ ...p, status: null, rank: null })); marcados.forEach(p => { if (p.proeminencia < cfg.prominenceMin) p.status = 'sem_proeminencia'; }); const candidatos = marcados .filter(p => !p.status) .sort((a, b) => b.massa - a.massa); if (candidatos.length === 0) { return { selecionados: [], marcados }; } if (candidatos[0].massa < cfg.massMin) { candidatos.forEach(p => { p.status = 'primario_fraco'; }); return { selecionados: [], marcados }; } const selecionados = []; const massaPrim = candidatos[0].massa; candidatos[0].status = 'aprovado'; candidatos[0].rank = 0; selecionados.push(candidatos[0]); for (let i = 1; i < candidatos.length; i++) { const c = candidatos[i]; if (selecionados.length >= cfg.maxPGs) { c.status = 'teto'; continue; } if (c.massa < massaPrim * cfg.dominanceRatio) { c.status = 'dominancia'; continue; } const longe = selecionados.every(s => Math.sqrt((s.x - c.x) ** 2 + (s.y - c.y) ** 2) >= cfg.minSep ); if (!longe) { c.status = 'separacao'; continue; } c.status = 'aprovado'; c.rank = selecionados.length; selecionados.push(c); } return { selecionados, marcados }; } // ========================================================================= // RENDERIZAÇÃO // ========================================================================= function renderizar() { if (!debugState.canvas) return; const ctx = debugState.canvas.getContext('2d'); const W = debugState.canvas.width; const H = debugState.canvas.height; ctx.clearRect(0, 0, W, H); const grid = debugState.grid; const maxV = debugState.maxGlobal || 1; const cellW = W / GRID_SIZE; const cellH = H / GRID_SIZE; for (let i = 0; i < GRID_SIZE; i++) { for (let j = 0; j < GRID_SIZE; j++) { const v = grid[i][j] / maxV; if (v < 0.05) continue; const cx = (1 - i / 100) * W; const cy = (j / 100) * H; ctx.fillStyle = corParaDensidade(v); ctx.fillRect(cx - cellW / 2, cy - cellH / 2, cellW + 1, cellH + 1); } } debugState.picosMarcados.forEach(pico => { const px = (1 - pico.x / 100) * W; const py = (pico.y / 100) * H; desenharMarcador(ctx, px, py, pico); }); } function desenharMarcador(ctx, x, y, pico) { let cor, raio, fill; switch (pico.status) { case 'aprovado': cor = '#00ff88'; raio = 13; fill = true; break; case 'sem_proeminencia': cor = '#888'; raio = 5; fill = false; break; default: cor = '#ffcc00'; raio = 9; fill = false; } ctx.beginPath(); ctx.arc(x, y, raio, 0, Math.PI * 2); ctx.strokeStyle = cor; ctx.lineWidth = 2.5; ctx.stroke(); if (fill) { ctx.fillStyle = cor; ctx.beginPath(); ctx.arc(x, y, 3.5, 0, Math.PI * 2); ctx.fill(); } const label = `${(pico.massa * 100).toFixed(0)}% · ${(pico.proeminencia * 100).toFixed(0)}%`; ctx.font = 'bold 10px Arial'; const textW = ctx.measureText(label).width; ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(x + raio + 3, y - 7, textW + 6, 13); ctx.fillStyle = cor; ctx.fillText(label, x + raio + 6, y + 3); } function corParaDensidade(v) { const stops = [ [0.00, [30, 50, 140, 0]], [0.15, [30, 100, 200, 80]], [0.35, [0, 200, 220, 130]], [0.55, [100, 220, 100, 160]], [0.75, [255, 200, 50, 180]], [0.90, [255, 80, 100, 200]], [1.00, [255, 0, 102, 220]] ]; for (let i = 1; i < stops.length; i++) { if (v <= stops[i][0]) { const [v0, c0] = stops[i - 1]; const [v1, c1] = stops[i]; const t = (v - v0) / (v1 - v0); const r = Math.round(c0[0] + t * (c1[0] - c0[0])); const g = Math.round(c0[1] + t * (c1[1] - c0[1])); const b = Math.round(c0[2] + t * (c1[2] - c0[2])); const a = (c0[3] + t * (c1[3] - c0[3])) / 255; return `rgba(${r},${g},${b},${a.toFixed(2)})`; } } const c = stops[stops.length - 1][1]; return `rgba(${c[0]},${c[1]},${c[2]},${c[3] / 255})`; } // ========================================================================= // ESTATÍSTICAS // ========================================================================= function atualizarStats() { const stats = document.getElementById('pg4d-stats'); if (!stats) return; const pontos = debugState.pontos.length; const picos = debugState.picosMarcados.length; const pGs = debugState.pGsFinais.length; let html = `
Pontos: ${pontos}
Picos brutos: ${picos}
pGs aprovados: ${pGs}
`; if (pGs === 0 && picos > 0) { html += `
Nenhum pG aprovado (dispersão ou filtros estritos).
`; } html += `
`; const sorted = [...debugState.picosMarcados].sort((a, b) => b.massa - a.massa); sorted.forEach((p, i) => { const cor = p.status === 'aprovado' ? '#00ff88' : p.status === 'sem_proeminencia' ? '#888' : '#ffcc00'; const rotuloStatus = traduzirStatus(p.status); html += `
pico ${i + 1} @ (${p.x}, ${p.y})
massa: ${(p.massa * 100).toFixed(1)}% · prom: ${(p.proeminencia * 100).toFixed(0)}%
${rotuloStatus}
`; }); stats.innerHTML = html; } function traduzirStatus(s) { return { aprovado: '✓ aprovado como pG', sem_proeminencia: `✗ proeminência < ${(DEBUG_CONFIG.prominenceMin * 100).toFixed(0)}%`, primario_fraco: `✗ massa < ${(DEBUG_CONFIG.massMin * 100).toFixed(0)}% (dispersão)`, dominancia: `✗ massa < ${(DEBUG_CONFIG.dominanceRatio * 100).toFixed(0)}% do primário`, separacao: '✗ próximo demais de outro pG', teto: `✗ já há ${DEBUG_CONFIG.maxPGs} pGs` }[s] || '?'; } // ========================================================================= // AÇÕES // ========================================================================= function copiarConfig() { const config = `// Cole em pg4.js substituindo as constantes: const BANDWIDTH = ${DEBUG_CONFIG.bandwidth}; const MASS_MIN_PRIMARY = ${DEBUG_CONFIG.massMin}; const DOMINANCE_RATIO = ${DEBUG_CONFIG.dominanceRatio}; const PROMINENCE_MIN = ${DEBUG_CONFIG.prominenceMin};`; navigator.clipboard.writeText(config).then(() => { const btn = document.getElementById('pg4d-copy'); const orig = btn.textContent; btn.textContent = 'Copiado!'; setTimeout(() => { btn.textContent = orig; }, 1500); }).catch(() => { prompt('Copie manualmente:', config); }); } // ========================================================================= // ESTILOS // ========================================================================= function injectStyles() { const style = document.createElement('style'); style.textContent = ` #pg4DebugPanel { position: fixed; top: 60px; right: 20px; width: 290px; max-height: calc(100vh - 80px); overflow-y: auto; background: rgba(25, 18, 60, 0.96); border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 12px; padding: 14px; color: white; font-family: Arial, sans-serif; font-size: 11px; z-index: 300; backdrop-filter: blur(10px); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); } .pg4d-header { display: flex; justify-content: space-between; align-items: center; font-weight: bold; font-size: 12px; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.12); text-transform: uppercase; letter-spacing: 0.5px; } .pg4d-close { background: transparent; border: none; color: white; cursor: pointer; font-size: 20px; padding: 0 6px; line-height: 1; } .pg4d-close:hover { color: #ff0066; } .pg4d-sliders { margin-bottom: 6px; } .pg4d-slider { display: block; margin-bottom: 12px; cursor: help; } .pg4d-slider-top { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 10px; } .pg4d-label { color: rgba(255, 255, 255, 0.85); letter-spacing: 0.5px; } .pg4d-val { color: #ff0066; font-weight: bold; font-family: monospace; } .pg4d-slider input[type=range] { width: 100%; height: 4px; background: rgba(255, 255, 255, 0.12); outline: none; -webkit-appearance: none; border-radius: 2px; } .pg4d-slider input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: #ff0066; border-radius: 50%; cursor: pointer; } .pg4d-slider input[type=range]::-moz-range-thumb { width: 14px; height: 14px; background: #ff0066; border-radius: 50%; cursor: pointer; border: none; } .pg4d-stats { padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); } .pg4d-row { margin-bottom: 4px; font-size: 11px; } .pg4d-warn { color: #ffcc00; font-style: italic; margin-top: 6px; } .pg4d-divider { height: 1px; background: rgba(255,255,255,0.1); margin: 10px 0; } .pg4d-peak { background: rgba(255, 255, 255, 0.04); padding: 7px 9px; margin-bottom: 6px; border-radius: 5px; border-left: 3px solid #888; font-size: 10px; line-height: 1.4; } .pg4d-peak-head { font-weight: bold; margin-bottom: 2px; color: rgba(255,255,255,0.9); } .pg4d-status { margin-top: 3px; opacity: 0.75; font-size: 9.5px; } .pg4d-legend { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); font-size: 10px; } .pg4d-legend > div { margin: 4px 0; display: flex; align-items: center; } .pg4d-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 8px; flex-shrink: 0; } .pg4d-dot-pg { background: #00ff88; border: 2px solid #00ff88; } .pg4d-dot-cand { border: 2px solid #ffcc00; } .pg4d-dot-weak { border: 2px solid #888; } .pg4d-footer { margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); } .pg4d-action { background: rgba(255, 0, 102, 0.15); border: 1px solid #ff0066; color: #ff0066; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 10px; width: 100%; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; transition: background 0.2s; } .pg4d-action:hover { background: rgba(255, 0, 102, 0.3); } `; document.head.appendChild(style); } })();