diff --git a/static/app.js b/static/app.js index d9c619c..e6b386a 100644 --- a/static/app.js +++ b/static/app.js @@ -109,8 +109,16 @@ createApp({ const targetA = this.baseA; for (let i = 0; i < 7; i++) { - if (Math.abs((currentB[i] || 0) - (targetB[i] || 0)) > 1e-8) return true; - if (Math.abs((currentA[i] || 0) - (targetA[i] || 0)) > 1e-8) return true; + const bVal = currentB[i] || 0; + const bTarget = targetB[i] || 0; + // 使用相對誤差,避免數值較大時 (如 K > 10000) 產生的精確度誤判 + const epsB = Math.max(1e-8, Math.abs(bTarget) * 1e-10); + if (Math.abs(bVal - bTarget) > epsB) return true; + + const aVal = currentA[i] || 0; + const aTarget = targetA[i] || 0; + const epsA = Math.max(1e-8, Math.abs(aTarget) * 1e-10); + if (Math.abs(aVal - aTarget) > epsA) return true; } return false; } @@ -235,6 +243,17 @@ createApp({ // 當文字改變時,反向計算滑桿位置 this.calcSlidersFromText(); }, + onlyNumberKey(e, allowDot = true) { + // 允許的控制鍵 + const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End']; + if (allowedKeys.includes(e.key) || e.ctrlKey || e.metaKey) return; + + // 檢查字元是否合法 + const charRegex = allowDot ? /[0-9,.\-\s]/ : /[0-9,\-\s]/; + if (!charRegex.test(e.key)) { + e.preventDefault(); + } + }, updateFromFixed(idx, type, val) { const intVal = parseInt(val); if (isNaN(intVal)) return; @@ -284,10 +303,17 @@ createApp({ this.applyFilterDesign(); }, resetFilterParams() { + this.systemGain = 1.0; + this.fixedOverrides = { a: {}, b: {} }; + this.bSliders = [0, 0, 0, 0, 0, 0, 0]; + this.aSliders = [0, 0, 0, 0, 0, 0]; + this.outOfRangeB = [false, false, false, false, false, false, false]; + this.outOfRangeA = [false, false, false, false, false, false, false]; + if (this.filterType === "(無) 手動自訂") { this.b_str = "1.0, 0.0, 0.0, 0.0"; this.a_str = "1.0, 0.0, 0.0, 0.0"; - this.updateFromText(); + this.updateBodePlot(); } else { this.params = { fc: 1000.0, order: 1, @@ -455,20 +481,26 @@ createApp({ const layout = { paper_bgcolor: 'transparent', plot_bgcolor: 'transparent', - margin: isSmallScreen ? { t: 80, b: 70, l: 55, r: 15 } : { t: 70, b: 60, l: 70, r: 30 }, - showlegend: true, - legend: { - orientation: 'h', - y: 1.1, - x: 0.5, - xanchor: 'center', - font: { size: isSmallScreen ? 10 : 12, color: textColor } - }, - grid: { rows: 2, columns: 1, pattern: 'independent', roworder: 'top to bottom', ygap: isSmallScreen ? 0.35 : 0.35 }, + margin: isSmallScreen ? { t: 80, b: 120, l: 55, r: 15 } : { t: 70, b: 120, l: 70, r: 30 }, + showlegend: false, + grid: { rows: 2, columns: 1, pattern: 'independent', roworder: 'top to bottom', ygap: isSmallScreen ? 0.35 : 0.3 }, font: { color: textColor, family: 'Inter, sans-serif' }, annotations: [ - { text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.2, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } }, - { text: 'Phase Response (Deg)', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.2, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } }, + { text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.15, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } }, + { text: 'Phase Response (Deg)', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.15, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } }, + + // 手動 Magnitude 圖例 (位於上方圖表下方) + { xref: 'paper', yref: 'paper', x: 0.35, y: isSmallScreen ? 0.52 : 0.54, text: '一一一', font: { color: isDark ? '#3b82f6' : '#2563eb', size: 10 }, showarrow: false }, + { xref: 'paper', yref: 'paper', x: 0.42, y: isSmallScreen ? 0.52 : 0.54, text: 'Ideal (Float)', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' }, + { xref: 'paper', yref: 'paper', x: 0.58, y: isSmallScreen ? 0.52 : 0.54, text: '· · · ·', font: { color: isDark ? '#10b981' : '#059669', size: 14 }, showarrow: false }, + { xref: 'paper', yref: 'paper', x: 0.63, y: isSmallScreen ? 0.52 : 0.54, text: `Fixed (Q${this.shiftBits})`, font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' }, + + // 手動 Phase 圖例 (位於下方圖表下方) + { xref: 'paper', yref: 'paper', x: 0.35, y: -0.15, text: '一一一', font: { color: isDark ? '#f97316' : '#ea580c', size: 10 }, showarrow: false }, + { xref: 'paper', yref: 'paper', x: 0.42, y: -0.15, text: 'Ideal Phase', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' }, + { xref: 'paper', yref: 'paper', x: 0.58, y: -0.15, text: '· · · ·', font: { color: isDark ? '#f43f5e' : '#e11d48', size: 14 }, showarrow: false }, + { xref: 'paper', yref: 'paper', x: 0.63, y: -0.15, text: 'Fixed Phase', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' }, + ...annotations ], xaxis: { ...xAxisCommon }, @@ -481,8 +513,8 @@ createApp({ const traceMagIdeal = { x: freq, y: magIdeal, name: 'Ideal (Float)', type: 'scatter', line: { color: isDark ? '#3b82f6' : '#2563eb', width: 2.5 } }; const traceMagFixed = { x: freq, y: magFixed, name: `Fixed (Q${this.shiftBits})`, type: 'scatter', line: { color: isDark ? '#10b981' : '#059669', width: 2, dash: 'dot' } }; - const tracePhaseIdeal = { x: freq, y: phaseIdeal, name: 'Ideal Phase', type: 'scatter', line: { color: isDark ? '#f97316' : '#ea580c', width: 2.5 }, xaxis: 'x2', yaxis: 'y2', showlegend: false }; - const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2', showlegend: false }; + const tracePhaseIdeal = { x: freq, y: phaseIdeal, name: 'Ideal Phase', type: 'scatter', line: { color: isDark ? '#f97316' : '#ea580c', width: 2.5 }, xaxis: 'x2', yaxis: 'y2' }; + const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2' }; Plotly.react('bodePlot', [traceMagIdeal, traceMagFixed, tracePhaseIdeal, tracePhaseFixed], layout, { responsive: true }); }, diff --git a/static/index.html b/static/index.html index e7f242f..feeb2ca 100644 --- a/static/index.html +++ b/static/index.html @@ -235,11 +235,13 @@ (Hz){{ formatK(fs) }}
Scaling: 2^{{ shiftBits }} ({{ Math.pow(2, shiftBits) }})
@@ -381,7 +383,7 @@ :class="{ 'italic': !item.isOverridden }">{{ item.val }}