feat: add support for dynamic slider range configuration in UI and settings
This commit is contained in:
+14
@@ -73,6 +73,9 @@ class DesignParams(BaseModel):
|
||||
notch_q: float = Field(default=1.0, gt=0)
|
||||
opoz_fz: float = Field(default=15000.0, gt=0)
|
||||
opoz_fp: float = Field(default=10.0, gt=0)
|
||||
tp1z_fz: float = Field(default=200.0, gt=0)
|
||||
tp1z_fp1: float = Field(default=10.0, gt=0)
|
||||
tp1z_fp2: float = Field(default=5000.0, gt=0)
|
||||
tptz_fz1: float = Field(default=200.0, gt=0)
|
||||
tptz_fz2: float = Field(default=25000.0, gt=0)
|
||||
tptz_fp1: float = Field(default=10.0, gt=0)
|
||||
@@ -187,6 +190,17 @@ def design_filter(params: DesignParams):
|
||||
b_new, a_new = signal.bilinear(b_s, a_s, fs=fs_val)
|
||||
dc_gain = np.sum(b_new) / np.sum(a_new)
|
||||
b_new = b_new / dc_gain
|
||||
elif f_type == "2P1Z (二極一零)":
|
||||
validate_frequency("2P1Z 零點頻率", params.tp1z_fz, fs_val, below_nyquist=False)
|
||||
validate_frequency("2P1Z 極點頻率 1", params.tp1z_fp1, fs_val, below_nyquist=False)
|
||||
validate_frequency("2P1Z 極點頻率 2", params.tp1z_fp2, fs_val, below_nyquist=False)
|
||||
w_z = 2 * np.pi * params.tp1z_fz
|
||||
w_p1, w_p2 = 2 * np.pi * params.tp1z_fp1, 2 * np.pi * params.tp1z_fp2
|
||||
b_s = [1.0, w_z]
|
||||
a_s = [1.0, w_p1 + w_p2, w_p1 * w_p2]
|
||||
b_new, a_new = signal.bilinear(b_s, a_s, fs=fs_val)
|
||||
dc_gain = np.sum(b_new) / np.sum(a_new)
|
||||
b_new = b_new / dc_gain
|
||||
elif f_type == "2P2Z (二極二零)":
|
||||
validate_frequency("2P2Z 極點頻率 1", params.tptz_fp1, fs_val, below_nyquist=False)
|
||||
validate_frequency("2P2Z 極點頻率 2", params.tptz_fp2, fs_val, below_nyquist=False)
|
||||
|
||||
+98
-48
@@ -336,14 +336,14 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 進階係數關聯調整 (X = a1 - a2) -->
|
||||
<!-- 進階 a1/a2 微調 -->
|
||||
<div class="mt-4">
|
||||
<button v-if="!showAdvancedAdjustment" @click="enableAdvancedAdjustment"
|
||||
class="w-full py-3 border-2 border-dashed border-slate-200 dark:border-gray-800 rounded-xl text-slate-400 dark:text-gray-500 text-xs font-bold hover:bg-slate-50 dark:hover:bg-gray-800/50 hover:border-indigo-300 dark:hover:border-indigo-900 transition-all flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||
</svg>
|
||||
開啟進階係數關聯調整 (a1, a2)
|
||||
開啟 a1/a2 步階微調
|
||||
</button>
|
||||
|
||||
<div v-if="showAdvancedAdjustment" class="p-4 bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-800 rounded-xl shadow-inner">
|
||||
@@ -351,47 +351,77 @@
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
進階係數關聯調整
|
||||
a1/a2 步階微調
|
||||
</h3>
|
||||
|
||||
<!-- 差距調整 (X) -->
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 flex justify-between mb-2">
|
||||
<span>差距 (a1 - a2)</span>
|
||||
<span class="text-indigo-600 dark:text-indigo-400 font-mono bg-indigo-50 dark:bg-indigo-900/30 px-2 py-0.5 rounded">{{ a1a2Diff.toFixed(3) }}</span>
|
||||
</label>
|
||||
<input type="range" v-model.number="a1a2Diff" min="-4" max="4" step="0.001"
|
||||
@input="updateA1A2FromRelationship"
|
||||
class="w-full accent-indigo-500 h-1.5 bg-slate-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 mb-2">
|
||||
{{ isTouchInput ? '觸控拖曳微調' : '直接輸入 / 滾動微調' }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label v-if="!isTouchInput" class="block">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a1</span>
|
||||
<input type="number" :value="formatFineCoeff(currentA1)" step="0.001"
|
||||
@change="setAFineDirect(1, $event.target.value)" @keydown="onlyNumberKey"
|
||||
@wheel.prevent="handleAFineWheel(1, $event)"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm font-mono text-center text-indigo-700 dark:text-indigo-300">
|
||||
</label>
|
||||
<button v-else type="button" @pointerdown.prevent="startAFineDrag(1, $event)"
|
||||
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center active:border-indigo-400 active:ring-2 active:ring-indigo-400/20">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a1</span>
|
||||
<span class="text-lg font-mono font-bold text-indigo-700 dark:text-indigo-300">{{ formatFineCoeff(currentA1) }}</span>
|
||||
</button>
|
||||
<label v-if="!isTouchInput" class="block">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a2</span>
|
||||
<input type="number" :value="formatFineCoeff(currentA2)" step="0.001"
|
||||
@change="setAFineDirect(2, $event.target.value)" @keydown="onlyNumberKey"
|
||||
@wheel.prevent="handleAFineWheel(2, $event)"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm font-mono text-center text-emerald-700 dark:text-emerald-300">
|
||||
</label>
|
||||
<button v-else type="button" @pointerdown.prevent="startAFineDrag(2, $event)"
|
||||
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center active:border-emerald-400 active:ring-2 active:ring-emerald-400/20">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a2</span>
|
||||
<span class="text-lg font-mono font-bold text-emerald-700 dark:text-emerald-300">{{ formatFineCoeff(currentA2) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平移調整 (S) -->
|
||||
<div class="mb-4">
|
||||
<label class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 flex justify-between mb-2">
|
||||
<span>平移 (a1 + a2)</span>
|
||||
<span class="text-emerald-600 dark:text-emerald-400 font-mono bg-emerald-50 dark:bg-emerald-900/30 px-2 py-0.5 rounded">{{ a1a2Sum.toFixed(3) }}</span>
|
||||
</label>
|
||||
<input type="range" v-model.number="a1a2Sum" min="-4" max="4" step="0.001"
|
||||
@input="updateA1A2FromRelationship"
|
||||
class="w-full accent-emerald-500 h-1.5 bg-slate-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||
<div class="bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">目前 a1</span>
|
||||
<span class="text-sm font-mono font-bold text-indigo-600 dark:text-indigo-400">{{ formatFineCoeff(currentA1) }}</span>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">目前 a2</span>
|
||||
<span class="text-sm font-mono font-bold text-emerald-600 dark:text-emerald-400">{{ formatFineCoeff(currentA2) }}</span>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">差值 a1-a2</span>
|
||||
<span class="text-xs font-mono text-slate-600 dark:text-gray-300">{{ formatFineCoeff(currentA1A2Diff) }}</span>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">總和 a1+a2</span>
|
||||
<span class="text-xs font-mono text-slate-600 dark:text-gray-300">{{ formatFineCoeff(currentA1A2Sum) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 預覽結果 -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="flex-1 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a1 變化預覽</span>
|
||||
<div class="flex items-center justify-center gap-1.5">
|
||||
<span class="text-[10px] font-mono text-slate-400">{{ currentA1.toFixed(3) }}</span>
|
||||
<svg class="w-2.5 h-2.5 text-slate-300" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
<span class="text-xs font-mono font-bold text-indigo-600 dark:text-indigo-400">{{ previewA1.toFixed(3) }}</span>
|
||||
<div class="space-y-3 mb-4">
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 mb-1">共同平移</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button @pointerdown.prevent="startA1A2Repeat('common', -1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
|
||||
class="rounded-lg bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 text-slate-700 dark:text-gray-300 py-2 text-xs font-bold transition-colors">a1 - 0.001, a2 - 0.001</button>
|
||||
<button @pointerdown.prevent="startA1A2Repeat('common', 1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
|
||||
class="rounded-lg bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 text-slate-700 dark:text-gray-300 py-2 text-xs font-bold transition-colors">a1 + 0.001, a2 + 0.001</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
|
||||
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a2 變化預覽</span>
|
||||
<div class="flex items-center justify-center gap-1.5">
|
||||
<span class="text-[10px] font-mono text-slate-400">{{ currentA2.toFixed(3) }}</span>
|
||||
<svg class="w-2.5 h-2.5 text-slate-300" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
<span class="text-xs font-mono font-bold text-emerald-600 dark:text-emerald-400">{{ previewA2.toFixed(3) }}</span>
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 mb-1">差值調整</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button @pointerdown.prevent="startA1A2Repeat('spread', 1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
|
||||
class="rounded-lg bg-indigo-100 dark:bg-indigo-900/30 hover:bg-indigo-200 dark:hover:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 py-2 text-xs font-bold transition-colors">展開差值</button>
|
||||
<button @pointerdown.prevent="startA1A2Repeat('converge', 1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
|
||||
class="rounded-lg bg-emerald-100 dark:bg-emerald-900/30 hover:bg-emerald-200 dark:hover:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 py-2 text-xs font-bold transition-colors">收斂差值</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,7 +429,7 @@
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-100 dark:border-amber-800/30 rounded-lg p-3">
|
||||
<p class="text-[10px] text-amber-700 dark:text-amber-400 leading-relaxed">
|
||||
<span class="font-bold">運算說明:</span><br>
|
||||
調整時,系統會先將現有係數四捨五入至三位小數再計算基準值。調整結果會自動將 a1, a2 存回為三位小數精度。
|
||||
係數固定對齊到小數點後第三位。微調後請按「套用」更新圖表;觸控時按住數值面板上下拖曳。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -415,8 +445,11 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span>定點數係數轉換</span>
|
||||
<span
|
||||
class="text-xs bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded">Q{{
|
||||
shiftBits }}</span>
|
||||
class="text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">b Q{{
|
||||
shiftBitsB }}</span>
|
||||
<span
|
||||
class="text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 px-2 py-1 rounded">a Q{{
|
||||
shiftBitsA }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@@ -428,16 +461,33 @@
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">左移位元數 (Shift Bits:
|
||||
0~14)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" v-model.number="shiftBits" min="0" max="14"
|
||||
@change="clearFixedOverrides" @keydown="onlyNumberKey"
|
||||
<div class="mb-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">b 左移位元數 (Shift Bits)</label>
|
||||
<input v-if="!isTouchInput" type="number" :value="shiftBitsB" min="0"
|
||||
@change="setShiftBits('b', $event.target.value)" @keydown="onlyNumberKey"
|
||||
@wheel.prevent="handleShiftBitWheel('b', $event)"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
|
||||
<button v-else type="button" @pointerdown.prevent="startShiftBitDrag('b', $event)"
|
||||
class="touch-none select-none block w-24 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center active:border-blue-400 active:ring-2 active:ring-blue-400/20">
|
||||
<span class="text-lg font-mono font-bold text-blue-700 dark:text-blue-300">{{ shiftBitsB }}</span>
|
||||
</button>
|
||||
<p class="text-[10px] text-slate-400 mt-1">b scaling: 2^{{ shiftBitsB }} ({{
|
||||
Math.pow(2, shiftBitsB) }})</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">a 左移位元數 (Shift Bits)</label>
|
||||
<input v-if="!isTouchInput" type="number" :value="shiftBitsA" min="0"
|
||||
@change="setShiftBits('a', $event.target.value)" @keydown="onlyNumberKey"
|
||||
@wheel.prevent="handleShiftBitWheel('a', $event)"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
|
||||
<button v-else type="button" @pointerdown.prevent="startShiftBitDrag('a', $event)"
|
||||
class="touch-none select-none block w-24 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center active:border-orange-400 active:ring-2 active:ring-orange-400/20">
|
||||
<span class="text-lg font-mono font-bold text-orange-700 dark:text-orange-300">{{ shiftBitsA }}</span>
|
||||
</button>
|
||||
<p class="text-[10px] text-slate-400 mt-1">a scaling: 2^{{ shiftBitsA }} ({{
|
||||
Math.pow(2, shiftBitsA) }})</p>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Scaling: 2^{{ shiftBits }} ({{ Math.pow(2, shiftBits)
|
||||
}})</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -472,8 +522,8 @@
|
||||
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-orange-700 dark:text-orange-300"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 dark:text-gray-500 mt-3 italic">註:此數值為原始係數乘以 2^{{ shiftBits }}
|
||||
後四捨五入之結果,可用於定點數 DSP 實作。</p>
|
||||
<p class="text-[10px] text-slate-400 dark:text-gray-500 mt-3 italic">註:b 係數乘以 2^{{ shiftBitsB }},a
|
||||
係數乘以 2^{{ shiftBitsA }} 後四捨五入,可用於定點數 DSP 實作。</p>
|
||||
</section>
|
||||
|
||||
</aside>
|
||||
|
||||
+211
-51
@@ -33,12 +33,17 @@ export default {
|
||||
csvFile: null, csvColumns: [], csvPreview: [],
|
||||
selectedColumn: 0, loadingFilter: false, filterDone: false, timePlotData: null,
|
||||
mobileTab: 'settings', // 手機版頁籤:'settings' | 'chart'
|
||||
shiftBits: 14,
|
||||
shiftBitsB: 14,
|
||||
shiftBitsA: 14,
|
||||
fixedOverrides: { a: {}, b: {} },
|
||||
outOfRangeB: [false, false, false, false, false, false, false],
|
||||
outOfRangeA: [false, false, false, false, false, false, false],
|
||||
a1a2Diff: 0.0,
|
||||
a1a2Sum: 0.0,
|
||||
aFineStep: 0.001,
|
||||
aFineRepeatDelayTimer: null,
|
||||
aFineRepeatTimer: null,
|
||||
aFineDrag: null,
|
||||
shiftBitDrag: null,
|
||||
isTouchInput: false,
|
||||
showAdvancedAdjustment: false,
|
||||
}
|
||||
},
|
||||
@@ -80,17 +85,18 @@ export default {
|
||||
}
|
||||
},
|
||||
fixedPointCoeffs() {
|
||||
const scale = Math.pow(2, this.shiftBits);
|
||||
const scaleB = Math.pow(2, this.shiftBitsB);
|
||||
const scaleA = Math.pow(2, this.shiftBitsA);
|
||||
const b_raw = this.parseCoeffs(this.b_str);
|
||||
const a_raw = this.parseCoeffs(this.a_str);
|
||||
|
||||
const b = b_raw.map((x, i) => {
|
||||
const autoVal = Math.round(x * scale);
|
||||
const autoVal = Math.round(x * scaleB);
|
||||
const val = (this.fixedOverrides.b[i] !== undefined) ? this.fixedOverrides.b[i] : autoVal;
|
||||
return { label: `b${i}`, val, isOverridden: this.fixedOverrides.b[i] !== undefined };
|
||||
});
|
||||
const a = a_raw.map((x, i) => {
|
||||
const autoVal = Math.round(x * scale);
|
||||
const autoVal = Math.round(x * scaleA);
|
||||
const val = (this.fixedOverrides.a[i] !== undefined) ? this.fixedOverrides.a[i] : autoVal;
|
||||
return { label: `a${i}`, val, isOverridden: this.fixedOverrides.a[i] !== undefined };
|
||||
});
|
||||
@@ -123,26 +129,25 @@ export default {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
previewA1() {
|
||||
const S = parseFloat(this.a1a2Sum || 0);
|
||||
const D = parseFloat(this.a1a2Diff || 0);
|
||||
return Math.round(((S + D) / 2) * 1000) / 1000;
|
||||
},
|
||||
previewA2() {
|
||||
const S = parseFloat(this.a1a2Sum || 0);
|
||||
const D = parseFloat(this.a1a2Diff || 0);
|
||||
return Math.round(((S - D) / 2) * 1000) / 1000;
|
||||
},
|
||||
currentA1() {
|
||||
const a = this.parseCoeffs(this.a_str);
|
||||
return Math.round((a[1] || 0) * 1000) / 1000;
|
||||
return this.roundToStep(a[1] || 0, this.aFineStep);
|
||||
},
|
||||
currentA2() {
|
||||
const a = this.parseCoeffs(this.a_str);
|
||||
return Math.round((a[2] || 0) * 1000) / 1000;
|
||||
return this.roundToStep(a[2] || 0, this.aFineStep);
|
||||
},
|
||||
currentA1A2Diff() {
|
||||
return this.roundToStep(this.currentA1 - this.currentA2, this.aFineStep);
|
||||
},
|
||||
currentA1A2Sum() {
|
||||
return this.roundToStep(this.currentA1 + this.currentA2, this.aFineStep);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isTouchInput = window.matchMedia?.('(pointer: coarse)').matches || false;
|
||||
window.addEventListener('pointerdown', this.rememberPointerType);
|
||||
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
this.isDarkMode = savedTheme === 'dark';
|
||||
@@ -154,7 +159,20 @@ export default {
|
||||
}
|
||||
this.applyFilterDesign();
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('pointerdown', this.rememberPointerType);
|
||||
this.stopA1A2Repeat();
|
||||
this.stopAFineDrag();
|
||||
this.stopShiftBitDrag();
|
||||
},
|
||||
methods: {
|
||||
rememberPointerType(event) {
|
||||
if (event.pointerType === 'touch') {
|
||||
this.isTouchInput = true;
|
||||
} else if (event.pointerType === 'mouse' || event.pointerType === 'pen') {
|
||||
this.isTouchInput = false;
|
||||
}
|
||||
},
|
||||
toggleDarkMode() {
|
||||
this.isDarkMode = !this.isDarkMode;
|
||||
localStorage.setItem('theme', this.isDarkMode ? 'dark' : 'light');
|
||||
@@ -163,7 +181,7 @@ export default {
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
this.updateBodePlot();
|
||||
this.updateBodePlot({ switchToChart: false });
|
||||
if (this.timePlotData) {
|
||||
this.drawTimePlot(this.timePlotData);
|
||||
}
|
||||
@@ -210,7 +228,7 @@ export default {
|
||||
this.a_str = this.baseA.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
this.showAdvancedAdjustment = false;
|
||||
this.calcSlidersFromText();
|
||||
this.updateBodePlot();
|
||||
this.updateBodePlot({ switchToChart: false });
|
||||
},
|
||||
updateFromControls() {
|
||||
const currentB = this.baseB.map((base, i) => base * (this.systemGain || 1.0) * Math.pow(10, this.bSliders[i] || 0));
|
||||
@@ -224,21 +242,163 @@ export default {
|
||||
updateFromText() {
|
||||
// 不再呼叫 syncBaseFromText(),保留基礎設計 (baseB/baseA) 作為參考點
|
||||
// 只更新圖表,讓滑桿維持在相對應的偏差位置
|
||||
this.updateBodePlot();
|
||||
this.updateBodePlot({ switchToChart: false });
|
||||
},
|
||||
updateA1A2FromRelationship() {
|
||||
stepDecimals(step) {
|
||||
const text = String(step);
|
||||
if (text.includes('e-')) return Number(text.split('e-')[1]);
|
||||
return text.includes('.') ? text.split('.')[1].length : 0;
|
||||
},
|
||||
roundToStep(value, step = this.aFineStep) {
|
||||
const scale = 1 / step;
|
||||
return Math.round(value * scale) / scale;
|
||||
},
|
||||
formatFineCoeff(value) {
|
||||
return this.roundToStep(value, this.aFineStep).toFixed(this.stepDecimals(this.aFineStep));
|
||||
},
|
||||
setAFineDirect(index, rawValue) {
|
||||
const nextValue = parseFloat(rawValue);
|
||||
if (!Number.isFinite(nextValue)) return;
|
||||
if (index === 1) {
|
||||
this.setA1A2(nextValue, this.currentA2);
|
||||
} else {
|
||||
this.setA1A2(this.currentA1, nextValue);
|
||||
}
|
||||
},
|
||||
adjustAFineValue(index, steps) {
|
||||
const delta = steps * this.aFineStep;
|
||||
if (index === 1) {
|
||||
this.setA1A2(this.currentA1 + delta, this.currentA2);
|
||||
} else {
|
||||
this.setA1A2(this.currentA1, this.currentA2 + delta);
|
||||
}
|
||||
},
|
||||
handleAFineWheel(index, event) {
|
||||
const direction = event.deltaY < 0 ? 1 : -1;
|
||||
const steps = Math.max(1, Math.round(Math.abs(event.deltaY) / 80));
|
||||
this.adjustAFineValue(index, direction * steps);
|
||||
},
|
||||
startAFineDrag(index, event) {
|
||||
if (event.pointerType !== 'touch') return;
|
||||
event.preventDefault();
|
||||
this.aFineDrag = { index, lastY: event.clientY, remainder: 0 };
|
||||
window.addEventListener('pointermove', this.moveAFineDrag, { passive: false });
|
||||
window.addEventListener('pointerup', this.stopAFineDrag);
|
||||
window.addEventListener('pointercancel', this.stopAFineDrag);
|
||||
},
|
||||
moveAFineDrag(event) {
|
||||
if (!this.aFineDrag) return;
|
||||
event.preventDefault();
|
||||
const delta = this.aFineDrag.lastY - event.clientY;
|
||||
this.aFineDrag.lastY = event.clientY;
|
||||
this.aFineDrag.remainder += delta;
|
||||
const steps = Math.trunc(this.aFineDrag.remainder / 18);
|
||||
if (steps === 0) return;
|
||||
this.aFineDrag.remainder -= steps * 18;
|
||||
this.adjustAFineValue(this.aFineDrag.index, steps);
|
||||
},
|
||||
stopAFineDrag() {
|
||||
this.aFineDrag = null;
|
||||
window.removeEventListener('pointermove', this.moveAFineDrag);
|
||||
window.removeEventListener('pointerup', this.stopAFineDrag);
|
||||
window.removeEventListener('pointercancel', this.stopAFineDrag);
|
||||
},
|
||||
normalizeShiftBits(rawValue) {
|
||||
const nextValue = parseInt(rawValue, 10);
|
||||
if (!Number.isFinite(nextValue)) return 0;
|
||||
return Math.max(0, nextValue);
|
||||
},
|
||||
setShiftBits(type, rawValue) {
|
||||
const nextValue = this.normalizeShiftBits(rawValue);
|
||||
if (type === 'b') {
|
||||
this.shiftBitsB = nextValue;
|
||||
} else {
|
||||
this.shiftBitsA = nextValue;
|
||||
}
|
||||
this.clearFixedOverrides(type);
|
||||
},
|
||||
adjustShiftBits(type, steps) {
|
||||
const current = type === 'b' ? this.shiftBitsB : this.shiftBitsA;
|
||||
this.setShiftBits(type, current + steps);
|
||||
},
|
||||
handleShiftBitWheel(type, event) {
|
||||
const direction = event.deltaY < 0 ? 1 : -1;
|
||||
const steps = Math.max(1, Math.round(Math.abs(event.deltaY) / 80));
|
||||
this.adjustShiftBits(type, direction * steps);
|
||||
},
|
||||
startShiftBitDrag(type, event) {
|
||||
if (event.pointerType !== 'touch') return;
|
||||
event.preventDefault();
|
||||
this.shiftBitDrag = { type, lastY: event.clientY, remainder: 0 };
|
||||
window.addEventListener('pointermove', this.moveShiftBitDrag, { passive: false });
|
||||
window.addEventListener('pointerup', this.stopShiftBitDrag);
|
||||
window.addEventListener('pointercancel', this.stopShiftBitDrag);
|
||||
},
|
||||
moveShiftBitDrag(event) {
|
||||
if (!this.shiftBitDrag) return;
|
||||
event.preventDefault();
|
||||
const delta = this.shiftBitDrag.lastY - event.clientY;
|
||||
this.shiftBitDrag.lastY = event.clientY;
|
||||
this.shiftBitDrag.remainder += delta;
|
||||
const steps = Math.trunc(this.shiftBitDrag.remainder / 18);
|
||||
if (steps === 0) return;
|
||||
this.shiftBitDrag.remainder -= steps * 18;
|
||||
this.adjustShiftBits(this.shiftBitDrag.type, steps);
|
||||
},
|
||||
stopShiftBitDrag() {
|
||||
this.shiftBitDrag = null;
|
||||
window.removeEventListener('pointermove', this.moveShiftBitDrag);
|
||||
window.removeEventListener('pointerup', this.stopShiftBitDrag);
|
||||
window.removeEventListener('pointercancel', this.stopShiftBitDrag);
|
||||
},
|
||||
setA1A2(a1, a2) {
|
||||
const a = this.padTo7(this.parseCoeffs(this.a_str));
|
||||
const S = parseFloat(this.a1a2Sum || 0);
|
||||
const D = parseFloat(this.a1a2Diff || 0);
|
||||
|
||||
// a1 = (S + D) / 2, a2 = (S - D) / 2
|
||||
a[1] = Math.round(((S + D) / 2) * 1000) / 1000;
|
||||
a[2] = Math.round(((S - D) / 2) * 1000) / 1000;
|
||||
|
||||
a[0] = 1.0;
|
||||
a[1] = this.roundToStep(a1, this.aFineStep);
|
||||
a[2] = this.roundToStep(a2, this.aFineStep);
|
||||
this.a_str = a.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
// 同步其他滑桿位置
|
||||
this.calcSlidersFromText();
|
||||
},
|
||||
snapA1A2ToStep() {
|
||||
this.setA1A2(this.currentA1, this.currentA2);
|
||||
},
|
||||
adjustA1A2(mode, direction) {
|
||||
const step = this.aFineStep * direction;
|
||||
let a1 = this.currentA1;
|
||||
let a2 = this.currentA2;
|
||||
|
||||
if (mode === 'common') {
|
||||
a1 += step;
|
||||
a2 += step;
|
||||
} else if (mode === 'spread') {
|
||||
a1 -= step;
|
||||
a2 += step;
|
||||
} else if (mode === 'converge') {
|
||||
a1 += step;
|
||||
a2 -= step;
|
||||
} else if (mode === 'a1') {
|
||||
a1 += step;
|
||||
} else if (mode === 'a2') {
|
||||
a2 += step;
|
||||
}
|
||||
|
||||
this.setA1A2(a1, a2);
|
||||
},
|
||||
startA1A2Repeat(mode, direction) {
|
||||
this.stopA1A2Repeat();
|
||||
this.adjustA1A2(mode, direction);
|
||||
this.aFineRepeatDelayTimer = setTimeout(() => {
|
||||
this.aFineRepeatTimer = setInterval(() => {
|
||||
this.adjustA1A2(mode, direction);
|
||||
}, 80);
|
||||
}, 350);
|
||||
},
|
||||
stopA1A2Repeat() {
|
||||
clearTimeout(this.aFineRepeatDelayTimer);
|
||||
clearInterval(this.aFineRepeatTimer);
|
||||
this.aFineRepeatDelayTimer = null;
|
||||
this.aFineRepeatTimer = null;
|
||||
},
|
||||
enableAdvancedAdjustment() {
|
||||
this.calcSlidersFromText();
|
||||
this.showAdvancedAdjustment = true;
|
||||
@@ -278,11 +438,6 @@ export default {
|
||||
if (val !== 0 && base !== 0) this.outOfRangeA[i] = true;
|
||||
}
|
||||
}
|
||||
// 同步更新 a1a2Diff 與 a1a2Sum 顯示值
|
||||
const ra1 = Math.round((currentA[1] || 0) * 1000) / 1000;
|
||||
const ra2 = Math.round((currentA[2] || 0) * 1000) / 1000;
|
||||
this.a1a2Diff = ra1 - ra2;
|
||||
this.a1a2Sum = ra1 + ra2;
|
||||
},
|
||||
debouncedUpdateFromText(type) {
|
||||
if (type === 'b') this.b_str = this.b_str.replace(/[^0-9,.\-\s]/g, '');
|
||||
@@ -306,28 +461,32 @@ export default {
|
||||
const intVal = parseInt(val);
|
||||
if (isNaN(intVal)) return;
|
||||
this.fixedOverrides[type][idx] = intVal;
|
||||
this.updateBodePlot();
|
||||
this.updateBodePlot({ switchToChart: false });
|
||||
},
|
||||
clearFixedOverrides() {
|
||||
clearFixedOverrides(type = null) {
|
||||
if (type === 'a' || type === 'b') {
|
||||
this.fixedOverrides[type] = {};
|
||||
return;
|
||||
}
|
||||
this.fixedOverrides = { a: {}, b: {} };
|
||||
},
|
||||
onFsChange() {
|
||||
if (this.filterType !== "(無) 手動自訂") {
|
||||
this.applyFilterDesign();
|
||||
} else {
|
||||
this.updateBodePlot();
|
||||
this.updateBodePlot({ switchToChart: false });
|
||||
}
|
||||
},
|
||||
debouncedApply() {
|
||||
clearTimeout(this.bodeTimeout);
|
||||
this.bodeTimeout = setTimeout(() => {
|
||||
if (this.filterType !== "(無) 手動自訂") this.applyFilterDesign();
|
||||
else this.updateBodePlot();
|
||||
else this.updateBodePlot({ switchToChart: false });
|
||||
}, 300);
|
||||
},
|
||||
debouncedUpdateBode() {
|
||||
clearTimeout(this.bodeTimeout);
|
||||
this.bodeTimeout = setTimeout(() => this.updateBodePlot(), 150);
|
||||
this.bodeTimeout = setTimeout(() => this.updateBodePlot({ switchToChart: false }), 150);
|
||||
},
|
||||
onGainChange() {
|
||||
this.updateFromControls();
|
||||
@@ -361,7 +520,7 @@ export default {
|
||||
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.updateBodePlot();
|
||||
this.updateBodePlot({ switchToChart: false });
|
||||
} else {
|
||||
this.params = {
|
||||
fc: 1000.0, order: 1,
|
||||
@@ -412,7 +571,7 @@ export default {
|
||||
this.updateBodePlot();
|
||||
} catch (e) { this.globalError = e.message; }
|
||||
},
|
||||
async updateBodePlot() {
|
||||
async updateBodePlot({ switchToChart = true } = {}) {
|
||||
this.loadingBode = true;
|
||||
this.globalError = null;
|
||||
try {
|
||||
@@ -421,14 +580,15 @@ export default {
|
||||
if (b_ideal.length === 0) b_ideal = [1.0];
|
||||
if (a_ideal.length === 0) a_ideal = [1.0];
|
||||
|
||||
const scale = Math.pow(2, this.shiftBits);
|
||||
const scaleB = Math.pow(2, this.shiftBitsB);
|
||||
const scaleA = Math.pow(2, this.shiftBitsA);
|
||||
const b_fixed = b_ideal.map((x, i) => {
|
||||
const intVal = (this.fixedOverrides.b[i] !== undefined) ? this.fixedOverrides.b[i] : Math.round(x * scale);
|
||||
return intVal / scale;
|
||||
const intVal = (this.fixedOverrides.b[i] !== undefined) ? this.fixedOverrides.b[i] : Math.round(x * scaleB);
|
||||
return intVal / scaleB;
|
||||
});
|
||||
const a_fixed = a_ideal.map((x, i) => {
|
||||
const intVal = (this.fixedOverrides.a[i] !== undefined) ? this.fixedOverrides.a[i] : Math.round(x * scale);
|
||||
return intVal / scale;
|
||||
const intVal = (this.fixedOverrides.a[i] !== undefined) ? this.fixedOverrides.a[i] : Math.round(x * scaleA);
|
||||
return intVal / scaleA;
|
||||
});
|
||||
|
||||
const [resIdeal, resFixed] = await Promise.all([
|
||||
@@ -449,7 +609,7 @@ export default {
|
||||
|
||||
this.drawBodePlot(dataIdeal.freq, dataIdeal.mag, dataIdeal.phase, dataFixed.mag, dataFixed.phase);
|
||||
// 手機版:計算完成後自動切換到圖表頁籤
|
||||
if (window.innerWidth < 1024) this.mobileTab = 'chart';
|
||||
if (switchToChart && window.innerWidth < 1024) this.mobileTab = 'chart';
|
||||
} catch (e) { this.globalError = e.message; }
|
||||
finally { this.loadingBode = false; }
|
||||
},
|
||||
@@ -542,7 +702,7 @@ export default {
|
||||
{ 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' },
|
||||
{ xref: 'paper', yref: 'paper', x: 0.63, y: isSmallScreen ? 0.52 : 0.54, text: `Fixed (b Q${this.shiftBitsB}, a Q${this.shiftBitsA})`, 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 },
|
||||
@@ -560,7 +720,7 @@ export default {
|
||||
};
|
||||
|
||||
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 traceMagFixed = { x: freq, y: magFixed, name: `Fixed (b Q${this.shiftBitsB}, a Q${this.shiftBitsA})`, 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' };
|
||||
const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2' };
|
||||
@@ -660,4 +820,4 @@ export default {
|
||||
} catch (e) { this.globalError = e.message; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user