diff --git a/astro.config.mjs b/astro.config.mjs
index 8be07f9..772bc49 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -64,6 +64,7 @@ export default defineConfig({
items: [
{ label: 'Testing an Antenna', slug: 'tutorials/practical-projects/testing-antenna' },
{ label: 'Measuring a Filter', slug: 'tutorials/practical-projects/measuring-filter' },
+ { label: 'Designing Microstrip Filters', slug: 'tutorials/practical-projects/microstrip-filter-design' },
{ label: 'Cable Length with TDR', slug: 'tutorials/practical-projects/cable-tdr' },
],
},
diff --git a/src/components/FilterDesigner.astro b/src/components/FilterDesigner.astro
new file mode 100644
index 0000000..281a584
--- /dev/null
+++ b/src/components/FilterDesigner.astro
@@ -0,0 +1,653 @@
+---
+/**
+ * Interactive Microstrip Bandpass Filter Designer
+ *
+ * Uses a Web Worker for heavy calculations to keep UI responsive.
+ * Generates live SVG preview and downloadable KiCad PCB files.
+ */
+
+interface Preset {
+ name: string;
+ centerFreq: number;
+ bandwidth: number;
+ description: string;
+}
+
+const presets: Preset[] = [
+ { name: '915 MHz ISM', centerFreq: 915, bandwidth: 26, description: '902-928 MHz ISM band' },
+ { name: '433 MHz ISM', centerFreq: 433, bandwidth: 1.7, description: '433.05-434.79 MHz' },
+ { name: '1090 MHz ADS-B', centerFreq: 1090, bandwidth: 10, description: 'Aircraft tracking' },
+ { name: '2.4 GHz WiFi', centerFreq: 2450, bandwidth: 100, description: '2.4 GHz band' },
+];
+---
+
+
+
+
+
+
+
+
+
Live Preview
+
+
+
+
+
+
Calculated Dimensions
+
+
+
+ Resonator Width
+ —
+
+
+ Resonator Length
+ —
+
+
+ Coupling Gaps
+ —
+
+
+ Feed Width (50Ω)
+ —
+
+
+ Board Size
+ —
+
+
+ λg at f₀
+ —
+
+
+
+
+
+
+
+
+
+ Note: The KiCad file includes a solid ground plane on
+ the bottom copper layer (B.Cu). Open in KiCad 7+ and run DRC/zone fill.
+
+
+
+
+
+
+
+
+
+
diff --git a/src/content/docs/tutorials/practical-projects/microstrip-filter-design.mdx b/src/content/docs/tutorials/practical-projects/microstrip-filter-design.mdx
new file mode 100644
index 0000000..c657c76
--- /dev/null
+++ b/src/content/docs/tutorials/practical-projects/microstrip-filter-design.mdx
@@ -0,0 +1,202 @@
+---
+title: Designing Microstrip Bandpass Filters
+description: Design and build your own microstrip bandpass filters using your NanoVNA
+sidebar:
+ order: 4
+---
+
+import FilterDesigner from '../../../../components/FilterDesigner.astro';
+import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
+
+Learn how to design edge-coupled microstrip bandpass filters and fabricate them on standard FR4 PCB material. Your NanoVNA is the perfect tool for measuring and tuning these filters.
+
+## Why Build Your Own Filters?
+
+Commercial filters can be expensive, especially for non-standard frequencies. With some math and careful fabrication, you can create filters that:
+
+- Match your exact frequency requirements
+- Cost just a few dollars in PCB material
+- Can be iterated quickly for experimentation
+- Teach you fundamental RF design principles
+
+## Interactive Filter Designer
+
+Before we dive into the theory, here's an interactive calculator that generates filter dimensions and downloadable KiCad PCB files. Try adjusting the parameters to see how they affect the design:
+
+
+
+
+
+---
+
+## The Math Behind the Magic
+
+The calculator above uses well-established microwave engineering formulas. Let's walk through the key concepts.
+
+### Microstrip Fundamentals
+
+A microstrip line consists of a conductor trace on one side of a dielectric substrate, with a ground plane on the other side. The electromagnetic field propagates partly through the substrate and partly through air.
+
+```
+ W (trace width)
+ ├─────────────────┤
+ ┌─────────────────┐ ─┬─ copper (35μm typ.)
+ │ │ │
+════╧═════════════════╧═══╧═══ ─┬─ substrate (εr, h)
+ │
+══════════════════════════════ ─┴─ ground plane
+```
+
+### Effective Dielectric Constant
+
+Because the field exists in both the substrate and air, we use an **effective dielectric constant** (εeff) that's between 1 (air) and εr (substrate):
+
+```
+εeff = (εr + 1)/2 + (εr - 1)/2 × (1 + 12h/W)^(-0.5)
+```
+
+For FR4 (εr ≈ 4.4) with a 1.6mm substrate and 3mm wide trace:
+- εeff ≈ 3.3
+- This means signals travel at about 55% the speed of light
+
+### Characteristic Impedance
+
+The trace width determines the characteristic impedance. For a 50Ω line:
+
+**Narrow strips (W/h ≤ 1):**
+```
+Z₀ = (60 / √εeff) × ln(8h/W + W/4h)
+```
+
+**Wide strips (W/h > 1):**
+```
+Z₀ = (120π / √εeff) / (W/h + 1.393 + 0.667 × ln(W/h + 1.444))
+```
+
+### Guided Wavelength
+
+The wavelength in the microstrip (λg) is shorter than free-space wavelength:
+
+```
+λg = c / (f × √εeff)
+```
+
+At 915 MHz on FR4:
+- Free-space λ = 328 mm
+- Guided λg ≈ 180 mm (with εeff ≈ 3.3)
+
+### Coupled-Line Filter Theory
+
+Edge-coupled bandpass filters use quarter-wave or half-wave resonators that couple energy through fringing fields in the gaps between them. The coupling coefficient (k) determines bandwidth:
+
+```
+Z₀e = Z₀ × √((1 + k) / (1 - k)) (even mode)
+Z₀o = Z₀ × √((1 - k) / (1 + k)) (odd mode)
+```
+
+Where Z₀e and Z₀o are even and odd mode impedances that depend on the gap spacing.
+
+---
+
+## Fabrication Tips
+
+
+
+1. **Export to KiCad**
+
+ Use the "Download KiCad PCB" button above. The file is compatible with KiCad 7 and 8.
+
+2. **Verify dimensions**
+
+ Open in KiCad and measure the traces. The critical dimensions are:
+ - Resonator length (determines center frequency)
+ - Gap widths (determine bandwidth and coupling)
+ - Trace width (determines impedance)
+
+3. **Add SMA footprints**
+
+ The generated file has copper pads but no connectors. Add edge-launch SMA footprints at the input/output pads.
+
+4. **Order PCB**
+
+ Use a standard PCB service with:
+ - 1.6mm FR4 substrate
+ - 1oz (35μm) copper
+ - HASL or ENIG finish
+ - **No solder mask over the filter traces** (optional but improves performance)
+
+5. **Test and tune**
+
+ Measure S21 (insertion loss) and S11 (return loss) with your NanoVNA. If the center frequency is off, you can trim the resonator lengths slightly with a file or knife.
+
+
+
+---
+
+## Measuring Your Filter
+
+
+
+ Connect Port 1 to the filter input and Port 2 to the output. The S21 trace shows:
+ - **Passband**: Should be close to 0 dB (typically -1 to -3 dB loss)
+ - **Stopband**: Should be well below -20 dB
+ - **Bandwidth**: Measure the -3 dB points
+
+ A well-made filter will show steep skirts at the band edges.
+
+
+ Good return loss (< -10 dB) across the passband indicates proper matching. Look for:
+ - Multiple dips corresponding to each resonator
+ - Symmetrical response for a symmetric filter
+
+
+
+
+
+---
+
+## Design Trade-offs
+
+| Parameter | Effect of Increasing |
+|-----------|---------------------|
+| **Poles** | Sharper rolloff, more insertion loss, larger board |
+| **Bandwidth** | Larger gaps (easier to fabricate), less selectivity |
+| **εr** | Smaller physical size, more loss, tighter tolerances |
+| **Substrate thickness** | Wider traces, lower loss, larger board |
+
+---
+
+## Common Issues
+
+### Center frequency is too low
+- Resonators are too long
+- Effective εr is higher than expected (moisture in FR4, different material)
+- **Fix**: Trim resonator ends carefully
+
+### High insertion loss
+- Copper surface rough or oxidized
+- Solder mask over traces absorbing energy
+- Poor connector transitions
+- **Fix**: Use ENIG finish, remove solder mask from filter area
+
+### Poor stopband rejection
+- Insufficient coupling (gaps too large)
+- Board too small (radiation/coupling around edges)
+- **Fix**: Increase board margin, check gap dimensions
+
+---
+
+## Going Further
+
+Once you've mastered basic bandpass filters, explore:
+
+- **Hairpin filters**: Folded resonators for compact designs
+- **Interdigital filters**: Better stopband performance
+- **Combline filters**: For higher frequencies
+- **Stepped-impedance filters**: Low-pass and high-pass designs
+
+Your NanoVNA is the essential tool for iterating on these designs. Each measurement informs your next revision!
diff --git a/src/workers/filterWorker.ts b/src/workers/filterWorker.ts
new file mode 100644
index 0000000..3bf831a
--- /dev/null
+++ b/src/workers/filterWorker.ts
@@ -0,0 +1,796 @@
+/**
+ * Microstrip Bandpass Filter Design Web Worker
+ *
+ * Implements Wheeler/Hammerstad equations for microstrip calculations
+ * and coupled-line filter synthesis from Butterworth prototypes.
+ *
+ * Reference: Pozar, "Microwave Engineering", Chapter 8
+ */
+
+// ============================================================================
+// Type Definitions
+// ============================================================================
+
+export interface FilterParams {
+ centerFreq: number; // MHz
+ bandwidth: number; // MHz (3dB bandwidth)
+ substrateEr: number; // Dielectric constant
+ substrateH: number; // Substrate thickness in mm
+ poles: number; // Number of filter poles (2-5)
+ z0: number; // System impedance in ohms (usually 50)
+ copperThickness: number; // Copper thickness in mm (typically 0.035)
+}
+
+export interface FilterDimensions {
+ resonatorWidth: number; // mm
+ resonatorLength: number; // mm
+ gaps: number[]; // mm (gaps between resonators)
+ feedWidth: number; // mm (50-ohm feed line width)
+ boardWidth: number; // mm
+ boardLength: number; // mm
+ effectiveEr: number; // Calculated effective dielectric constant
+ wavelengthG: number; // Guided wavelength at center freq (mm)
+}
+
+export interface WorkerInput {
+ type: 'calculate';
+ params: FilterParams;
+}
+
+export interface WorkerOutput {
+ type: 'result' | 'error';
+ dimensions?: FilterDimensions;
+ svg?: string;
+ kicadPcb?: string;
+ error?: string;
+ warnings?: string[];
+}
+
+// ============================================================================
+// Physical Constants
+// ============================================================================
+
+const C0 = 299792458; // Speed of light in m/s
+
+// ============================================================================
+// Butterworth Filter Prototype Values (g-values)
+// ============================================================================
+
+// Butterworth lowpass prototype element values
+// Index corresponds to filter order (n)
+const BUTTERWORTH_G: Record = {
+ 2: [1.0, 1.4142, 1.4142, 1.0],
+ 3: [1.0, 1.0, 2.0, 1.0, 1.0],
+ 4: [1.0, 0.7654, 1.8478, 1.8478, 0.7654, 1.0],
+ 5: [1.0, 0.6180, 1.6180, 2.0, 1.6180, 0.6180, 1.0],
+};
+
+// ============================================================================
+// Microstrip Calculations (Wheeler/Hammerstad)
+// ============================================================================
+
+/**
+ * Calculate effective dielectric constant for microstrip
+ * Using Hammerstad-Jensen formula
+ */
+function effectiveDielectric(er: number, h: number, w: number): number {
+ const u = w / h;
+
+ // Hammerstad-Jensen formula
+ const a = 1 + (1 / 49) * Math.log(
+ (Math.pow(u, 4) + Math.pow(u / 52, 2)) /
+ (Math.pow(u, 4) + 0.432)
+ ) + (1 / 18.7) * Math.log(1 + Math.pow(u / 18.1, 3));
+
+ const b = 0.564 * Math.pow((er - 0.9) / (er + 3), 0.053);
+
+ const eEff = (er + 1) / 2 + ((er - 1) / 2) * Math.pow(1 + 10 / u, -a * b);
+
+ return eEff;
+}
+
+/**
+ * Calculate characteristic impedance of microstrip line
+ * Using Wheeler's formula (modified by Hammerstad)
+ */
+function microstripZ0(er: number, h: number, w: number): number {
+ const u = w / h;
+ const eEff = effectiveDielectric(er, h, w);
+
+ let z0: number;
+
+ if (u <= 1) {
+ // Narrow strip formula
+ const f = 6 + (2 * Math.PI - 6) * Math.exp(-Math.pow(30.666 / u, 0.7528));
+ z0 = (60 / Math.sqrt(eEff)) * Math.log(f / u + Math.sqrt(1 + Math.pow(2 / u, 2)));
+ } else {
+ // Wide strip formula
+ z0 = (120 * Math.PI / Math.sqrt(eEff)) /
+ (u + 1.393 + 0.667 * Math.log(u + 1.444));
+ }
+
+ return z0;
+}
+
+/**
+ * Calculate microstrip width for a given impedance
+ * Iterative solver using Newton-Raphson
+ */
+function widthForZ0(targetZ0: number, er: number, h: number): number {
+ // Initial guess using simplified formula
+ let w: number;
+ const A = (targetZ0 / 60) * Math.sqrt((er + 1) / 2) +
+ ((er - 1) / (er + 1)) * (0.23 + 0.11 / er);
+ const B = (377 * Math.PI) / (2 * targetZ0 * Math.sqrt(er));
+
+ if (A > 1.52) {
+ // Narrow strip initial guess
+ w = (8 * h * Math.exp(A)) / (Math.exp(2 * A) - 2);
+ } else {
+ // Wide strip initial guess
+ w = (2 * h / Math.PI) * (B - 1 - Math.log(2 * B - 1) +
+ ((er - 1) / (2 * er)) * (Math.log(B - 1) + 0.39 - 0.61 / er));
+ }
+
+ // Newton-Raphson refinement
+ for (let i = 0; i < 20; i++) {
+ const z = microstripZ0(er, h, w);
+ const dw = 0.001 * h;
+ const dz = microstripZ0(er, h, w + dw) - z;
+ const dzDw = dz / dw;
+
+ if (Math.abs(dzDw) < 1e-10) break;
+
+ const wNew = w - (z - targetZ0) / dzDw;
+ if (Math.abs(wNew - w) < 1e-6 * h) break;
+ w = Math.max(0.01 * h, wNew); // Keep width positive
+ }
+
+ return w;
+}
+
+/**
+ * Calculate even and odd mode impedances for coupled lines
+ * from coupling coefficient k
+ */
+function evenOddImpedances(z0: number, k: number): { z0e: number; z0o: number } {
+ // Ensure coupling coefficient is in valid range
+ const kClamped = Math.max(0.01, Math.min(0.99, k));
+
+ const z0e = z0 * Math.sqrt((1 + kClamped) / (1 - kClamped));
+ const z0o = z0 * Math.sqrt((1 - kClamped) / (1 + kClamped));
+
+ return { z0e, z0o };
+}
+
+/**
+ * Calculate coupled line gap for given even/odd mode impedances
+ * Using Garg & Bahl formulas
+ */
+function coupledLineGap(
+ z0e: number,
+ z0o: number,
+ er: number,
+ h: number,
+ w: number
+): number {
+ // Use voltage coupling coefficient
+ const k = (z0e - z0o) / (z0e + z0o);
+
+ // Empirical formula for gap (simplified)
+ // Real implementations would use full Garg-Bahl equations
+ const s = h * 0.1 * Math.exp(2.5 * (1 - k));
+
+ // Clamp to reasonable values
+ return Math.max(0.1, Math.min(s, 2 * h));
+}
+
+// ============================================================================
+// Filter Synthesis
+// ============================================================================
+
+/**
+ * Calculate coupling coefficients for bandpass filter
+ * from Butterworth prototype values
+ */
+function couplingCoefficients(
+ n: number,
+ fbw: number
+): number[] {
+ const g = BUTTERWORTH_G[n];
+ if (!g) throw new Error(`Filter order ${n} not supported (use 2-5)`);
+
+ const k: number[] = [];
+
+ // k01 and k(n,n+1) are external coupling (input/output)
+ // Internal couplings k(j,j+1) for j = 1 to n-1
+ for (let j = 0; j < n; j++) {
+ const kj = fbw / Math.sqrt(g[j + 1] * g[j + 2]);
+ k.push(kj);
+ }
+
+ return k;
+}
+
+/**
+ * Calculate external Q for input/output coupling
+ */
+function externalQ(n: number, fbw: number): number {
+ const g = BUTTERWORTH_G[n];
+ if (!g) throw new Error(`Filter order ${n} not supported`);
+
+ return g[1] / fbw;
+}
+
+// ============================================================================
+// Filter Design Main Function
+// ============================================================================
+
+function designFilter(params: FilterParams): {
+ dimensions: FilterDimensions;
+ warnings: string[];
+} {
+ const warnings: string[] = [];
+
+ const {
+ centerFreq,
+ bandwidth,
+ substrateEr,
+ substrateH,
+ poles,
+ z0,
+ } = params;
+
+ // Fractional bandwidth
+ const fbw = bandwidth / centerFreq;
+
+ // Calculate feed line width (50 ohm)
+ const feedWidth = widthForZ0(z0, substrateEr, substrateH);
+
+ // Calculate effective dielectric constant at 50 ohms
+ const eEff = effectiveDielectric(substrateEr, substrateH, feedWidth);
+
+ // Guided wavelength at center frequency (mm)
+ const freqHz = centerFreq * 1e6;
+ const wavelengthG = (C0 / freqHz / Math.sqrt(eEff)) * 1000;
+
+ // Resonator length is λg/4 for quarter-wave resonators
+ // or λg/2 for half-wave (we'll use half-wave for edge-coupled)
+ const resonatorLength = wavelengthG / 2;
+
+ // Use same width as feed for resonators (simplification)
+ // A more sophisticated design would optimize width
+ const resonatorWidth = feedWidth;
+
+ // Calculate coupling coefficients
+ const couplings = couplingCoefficients(poles, fbw);
+
+ // Convert coupling coefficients to gaps
+ const gaps: number[] = [];
+ for (let i = 0; i < poles; i++) {
+ const k = couplings[i];
+ const { z0e, z0o } = evenOddImpedances(z0, k);
+ const gap = coupledLineGap(z0e, z0o, substrateEr, substrateH, resonatorWidth);
+ gaps.push(gap);
+ }
+
+ // Add the output coupling gap (same as input for symmetric filter)
+ // gaps.push(gaps[0]); // Already included in couplings
+
+ // Board dimensions with margin
+ const margin = 5; // 5mm margin around filter
+ const feedLength = 10; // 10mm feed lines
+
+ const boardLength = 2 * feedLength + poles * resonatorLength +
+ gaps.reduce((a, b) => a + b, 0) + 2 * margin;
+
+ const boardWidth = resonatorWidth + 2 * margin + 10; // Extra for connectors
+
+ // Validation warnings
+ if (resonatorLength > 200) {
+ warnings.push(`Resonator length (${resonatorLength.toFixed(1)}mm) is very long. Consider higher frequency.`);
+ }
+
+ if (gaps.some(g => g < 0.15)) {
+ warnings.push(`Some gaps are below 0.15mm - may be difficult to fabricate.`);
+ }
+
+ if (resonatorWidth < 0.3) {
+ warnings.push(`Trace width (${resonatorWidth.toFixed(2)}mm) is very narrow.`);
+ }
+
+ return {
+ dimensions: {
+ resonatorWidth,
+ resonatorLength,
+ gaps,
+ feedWidth,
+ boardWidth,
+ boardLength,
+ effectiveEr: eEff,
+ wavelengthG,
+ },
+ warnings,
+ };
+}
+
+// ============================================================================
+// SVG Generation
+// ============================================================================
+
+function generateSVG(params: FilterParams, dims: FilterDimensions): string {
+ const {
+ resonatorWidth,
+ resonatorLength,
+ gaps,
+ feedWidth,
+ boardWidth,
+ boardLength,
+ } = dims;
+
+ const { poles } = params;
+
+ // Scale factor for SVG (pixels per mm)
+ const scale = 3;
+
+ // Margins in the SVG
+ const svgMargin = 20;
+
+ const svgWidth = boardLength * scale + 2 * svgMargin;
+ const svgHeight = boardWidth * scale + 2 * svgMargin;
+
+ // Board offset in SVG coordinates
+ const boardX = svgMargin;
+ const boardY = svgMargin;
+
+ // Center Y position for the filter
+ const centerY = boardY + (boardWidth * scale) / 2;
+
+ // Copper color (PCB gold/copper look)
+ const copperColor = '#c87533';
+ const boardColor = '#1a472a'; // PCB green
+ const silkColor = '#f0f0f0';
+ const groundColor = '#2d5a3d'; // Slightly lighter green for ground indication
+
+ let svg = ``;
+
+ return svg;
+}
+
+// ============================================================================
+// KiCad PCB Generation
+// ============================================================================
+
+function generateKicadPCB(params: FilterParams, dims: FilterDimensions): string {
+ const {
+ resonatorWidth,
+ resonatorLength,
+ gaps,
+ feedWidth,
+ boardWidth,
+ boardLength,
+ } = dims;
+
+ const { poles, centerFreq, copperThickness } = params;
+
+ // KiCad uses mm coordinates
+ const margin = 5;
+ const feedLen = 10;
+
+ // Generate unique timestamps
+ const timestamp = Date.now().toString(16);
+
+ let pcb = `(kicad_pcb
+ (version 20240108)
+ (generator "NanoVNA Filter Designer")
+ (generator_version "1.0")
+
+ (general
+ (thickness 1.6)
+ (legacy_teardrops no)
+ )
+
+ (paper "A4")
+
+ (layers
+ (0 "F.Cu" signal)
+ (31 "B.Cu" signal)
+ (32 "B.Adhes" user "B.Adhesive")
+ (33 "F.Adhes" user "F.Adhesive")
+ (34 "B.Paste" user)
+ (35 "F.Paste" user)
+ (36 "B.SilkS" user "B.Silkscreen")
+ (37 "F.SilkS" user "F.Silkscreen")
+ (38 "B.Mask" user)
+ (39 "F.Mask" user)
+ (40 "Dwgs.User" user "User.Drawings")
+ (41 "Cmts.User" user "User.Comments")
+ (42 "Eco1.User" user "User.Eco1")
+ (43 "Eco2.User" user "User.Eco2")
+ (44 "Edge.Cuts" user)
+ (45 "Margin" user)
+ (46 "B.CrtYd" user "B.Courtyard")
+ (47 "F.CrtYd" user "F.Courtyard")
+ (48 "B.Fab" user)
+ (49 "F.Fab" user)
+ (50 "User.1" user)
+ (51 "User.2" user)
+ (52 "User.3" user)
+ (53 "User.4" user)
+ (54 "User.5" user)
+ (55 "User.6" user)
+ (56 "User.7" user)
+ (57 "User.8" user)
+ (58 "User.9" user)
+ )
+
+ (setup
+ (pad_to_mask_clearance 0)
+ (allow_soldermask_bridges_in_footprints no)
+ (pcbplotparams
+ (layerselection 0x00010fc_ffffffff)
+ (plot_on_all_layers_selection 0x0000000_00000000)
+ (disableapertmacros no)
+ (usegerberextensions no)
+ (usegerberattributes yes)
+ (usegerberadvancedattributes yes)
+ (creategerberjobfile yes)
+ (dashed_line_dash_ratio 12.000000)
+ (dashed_line_gap_ratio 3.000000)
+ (svgprecision 4)
+ (plotframeref no)
+ (viasonmask no)
+ (mode 1)
+ (useauxorigin no)
+ (hpglpennumber 1)
+ (hpglpenspeed 20)
+ (hpglpendiameter 15.000000)
+ (pdf_front_fp_property_popups yes)
+ (pdf_back_fp_property_popups yes)
+ (dxfpolygonmode yes)
+ (dxfimperialunits yes)
+ (dxfusepcbnewfont yes)
+ (psnegative no)
+ (psa4output no)
+ (plotreference yes)
+ (plotvalue yes)
+ (plotfptext yes)
+ (plotinvisibletext no)
+ (sketchpadsonfab no)
+ (subtractmaskfromsilk no)
+ (outputformat 1)
+ (mirror no)
+ (drillshape 1)
+ (scaleselection 1)
+ (outputdirectory "")
+ )
+ )
+
+ (net 0 "")
+ (net 1 "GND")
+ (net 2 "RF_IN")
+ (net 3 "RF_OUT")
+
+`;
+
+ // Board outline (Edge.Cuts)
+ pcb += ` ;; Board Outline
+ (gr_rect
+ (start 0 0)
+ (end ${boardLength} ${boardWidth})
+ (stroke (width 0.15) (type default))
+ (fill none)
+ (layer "Edge.Cuts")
+ (uuid "${timestamp}01")
+ )
+
+`;
+
+ // Ground plane on B.Cu (solid copper fill)
+ pcb += ` ;; Ground Plane (B.Cu) - Solid copper fill
+ (zone
+ (net 1)
+ (net_name "GND")
+ (layer "B.Cu")
+ (uuid "${timestamp}02")
+ (hatch edge 0.5)
+ (connect_pads yes (clearance 0.3))
+ (min_thickness 0.25)
+ (filled_areas_thickness no)
+ (fill yes (thermal_gap 0.5) (thermal_bridge_width 0.5))
+ (polygon
+ (pts
+ (xy 0.5 0.5)
+ (xy ${boardLength - 0.5} 0.5)
+ (xy ${boardLength - 0.5} ${boardWidth - 0.5})
+ (xy 0.5 ${boardWidth - 0.5})
+ )
+ )
+ (filled_polygon
+ (layer "B.Cu")
+ (pts
+ (xy 0.5 0.5)
+ (xy ${boardLength - 0.5} 0.5)
+ (xy ${boardLength - 0.5} ${boardWidth - 0.5})
+ (xy 0.5 ${boardWidth - 0.5})
+ )
+ )
+ )
+
+`;
+
+ // Copper traces on F.Cu
+ let currentX = margin;
+ const centerY = boardWidth / 2;
+ const traceHalfW = resonatorWidth / 2;
+ const feedHalfW = feedWidth / 2;
+
+ // Input feed line
+ pcb += ` ;; Input Feed Line (F.Cu)
+ (gr_poly
+ (pts
+ (xy ${currentX} ${centerY - feedHalfW})
+ (xy ${currentX + feedLen} ${centerY - feedHalfW})
+ (xy ${currentX + feedLen} ${centerY + feedHalfW})
+ (xy ${currentX} ${centerY + feedHalfW})
+ )
+ (stroke (width 0) (type solid))
+ (fill solid)
+ (layer "F.Cu")
+ (uuid "${timestamp}10")
+ )
+
+`;
+
+ currentX += feedLen;
+
+ // Draw resonators
+ for (let i = 0; i < poles; i++) {
+ // Gap before resonator
+ const gapIdx = i === 0 ? 0 : i;
+ currentX += gaps[Math.min(gapIdx, gaps.length - 1)];
+
+ // Resonator
+ pcb += ` ;; Resonator ${i + 1} (F.Cu)
+ (gr_poly
+ (pts
+ (xy ${currentX} ${centerY - traceHalfW})
+ (xy ${currentX + resonatorLength} ${centerY - traceHalfW})
+ (xy ${currentX + resonatorLength} ${centerY + traceHalfW})
+ (xy ${currentX} ${centerY + traceHalfW})
+ )
+ (stroke (width 0) (type solid))
+ (fill solid)
+ (layer "F.Cu")
+ (uuid "${timestamp}2${i}")
+ )
+
+`;
+
+ currentX += resonatorLength;
+ }
+
+ // Output coupling gap
+ currentX += gaps[gaps.length - 1];
+
+ // Output feed line
+ pcb += ` ;; Output Feed Line (F.Cu)
+ (gr_poly
+ (pts
+ (xy ${currentX} ${centerY - feedHalfW})
+ (xy ${currentX + feedLen} ${centerY - feedHalfW})
+ (xy ${currentX + feedLen} ${centerY + feedHalfW})
+ (xy ${currentX} ${centerY + feedHalfW})
+ )
+ (stroke (width 0) (type solid))
+ (fill solid)
+ (layer "F.Cu")
+ (uuid "${timestamp}30")
+ )
+
+`;
+
+ // Silkscreen labels
+ pcb += ` ;; Silkscreen Labels
+ (gr_text "${centerFreq} MHz BPF"
+ (at ${boardLength / 2} 3)
+ (layer "F.SilkS")
+ (uuid "${timestamp}40")
+ (effects (font (size 1.5 1.5) (thickness 0.2)) (justify left bottom))
+ )
+
+ (gr_text "IN"
+ (at ${margin + 2} ${centerY + feedHalfW + 2})
+ (layer "F.SilkS")
+ (uuid "${timestamp}41")
+ (effects (font (size 1 1) (thickness 0.15)))
+ )
+
+ (gr_text "OUT"
+ (at ${boardLength - margin - 4} ${centerY + feedHalfW + 2})
+ (layer "F.SilkS")
+ (uuid "${timestamp}42")
+ (effects (font (size 1 1) (thickness 0.15)))
+ )
+
+ (gr_text "GND"
+ (at ${boardLength / 2} ${boardWidth - 2})
+ (layer "B.SilkS")
+ (uuid "${timestamp}43")
+ (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
+ )
+
+`;
+
+ pcb += `)\n`;
+
+ return pcb;
+}
+
+// ============================================================================
+// Web Worker Message Handler
+// ============================================================================
+
+self.onmessage = (event: MessageEvent) => {
+ const { type, params } = event.data;
+
+ if (type !== 'calculate') {
+ const response: WorkerOutput = {
+ type: 'error',
+ error: `Unknown message type: ${type}`,
+ };
+ self.postMessage(response);
+ return;
+ }
+
+ try {
+ // Validate inputs
+ if (params.centerFreq < 10 || params.centerFreq > 3000) {
+ throw new Error('Center frequency must be between 10 MHz and 3000 MHz');
+ }
+ if (params.bandwidth <= 0 || params.bandwidth > params.centerFreq) {
+ throw new Error('Bandwidth must be positive and less than center frequency');
+ }
+ if (params.poles < 2 || params.poles > 5) {
+ throw new Error('Number of poles must be between 2 and 5');
+ }
+ if (params.substrateEr < 1 || params.substrateEr > 15) {
+ throw new Error('Substrate εr must be between 1 and 15');
+ }
+ if (params.substrateH < 0.1 || params.substrateH > 5) {
+ throw new Error('Substrate thickness must be between 0.1 and 5 mm');
+ }
+
+ // Design the filter
+ const { dimensions, warnings } = designFilter(params);
+
+ // Generate SVG preview
+ const svg = generateSVG(params, dimensions);
+
+ // Generate KiCad PCB
+ const kicadPcb = generateKicadPCB(params, dimensions);
+
+ const response: WorkerOutput = {
+ type: 'result',
+ dimensions,
+ svg,
+ kicadPcb,
+ warnings,
+ };
+
+ self.postMessage(response);
+ } catch (error) {
+ const response: WorkerOutput = {
+ type: 'error',
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ self.postMessage(response);
+ }
+};
+
+// Export types for external use
+export type { FilterParams, FilterDimensions, WorkerInput, WorkerOutput };