diff --git a/app.py b/app.py index d7519a5..83ab02e 100644 --- a/app.py +++ b/app.py @@ -9,13 +9,14 @@ POST /api/sweep → run sweep_operating_points(), return JSON dataset """ import base64 +import json import math import os import tempfile from flask import Flask, render_template, request, jsonify -from designer import ToroidCore, WindingSpec, design_transformer +from designer import ToroidCore, WindingSpec, WireSpec, design_transformer from sim_toroid import ( ToroidSimulator, SimConstraints, sweep_operating_points, SweepEntry, @@ -61,12 +62,30 @@ def _parse_windings(data: dict) -> list[WindingSpec]: return specs -def _drawing_b64(core: ToroidCore, results) -> str: +def _actual_fill_factor(core: ToroidCore, results) -> float: + """ + Compute actual fill factor = total wire cross-section area placed / window area. + Sums wire area for every turn placed across all windings. + """ + window_area = core.window_area_mm2 + if window_area <= 0: + return 0.0 + total_wire_area = 0.0 + for wr in results: + for seg in wr.segments: + wire = WireSpec.from_awg(seg.awg) + r = wire.diameter_mm / 2.0 + total_wire_area += math.pi * r * r * seg.turns + return total_wire_area / window_area + + +def _drawing_b64(core: ToroidCore, results, actual_fill: float) -> str: """Render the toroid PNG and return it as a base64 data-URI string.""" with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: tmp_path = f.name try: - draw_toroid(core, results, output_path=tmp_path, dpi=150, fig_size_mm=160) + draw_toroid(core, results, output_path=tmp_path, dpi=150, fig_size_mm=160, + actual_fill=actual_fill) with open(tmp_path, "rb") as f: png_bytes = f.read() finally: @@ -156,13 +175,28 @@ def api_design(): results = design_transformer(core, specs, fill_factor=fill_factor) - drawing_b64 = _drawing_b64(core, results) + actual_fill = _actual_fill_factor(core, results) + drawing_b64 = _drawing_b64(core, results, actual_fill) windings_info = [_winding_result_to_dict(wr) for wr in results] + # Thermal surface area of wound toroid (m²): + # outer cylinder + top/bottom annular faces + inner bore cylinder + OD_m = core.OD_mm * 1e-3 + ID_m = core.ID_mm * 1e-3 + H_m = core.height_mm * 1e-3 + A_outer = math.pi * OD_m * H_m + A_faces = 2 * math.pi / 4 * (OD_m**2 - ID_m**2) + A_bore = math.pi * ID_m * H_m + A_surface_m2 = A_outer + A_faces + A_bore + return jsonify({ "success": True, "windings": windings_info, "drawing": drawing_b64, + "fill_factor_target": fill_factor, + "fill_factor_actual": round(actual_fill, 4), + "window_area_mm2": round(core.window_area_mm2, 2), + "A_surface_m2": round(A_surface_m2, 6), }) except Exception as exc: @@ -259,5 +293,93 @@ def api_sweep(): "traceback": traceback.format_exc()}), 400 +# --------------------------------------------------------------------------- +# Preset persistence +# --------------------------------------------------------------------------- + +PRESETS_DIR = os.path.join(os.path.dirname(__file__), "presets") +os.makedirs(PRESETS_DIR, exist_ok=True) + +# Valid preset categories +_PRESET_TYPES = {"core", "windings", "constraints", "sim"} + + +def _preset_path(ptype: str) -> str: + return os.path.join(PRESETS_DIR, f"{ptype}.json") + + +def _load_store(ptype: str) -> dict: + """Load the JSON store for a preset type; return {} on missing/corrupt.""" + path = _preset_path(ptype) + if not os.path.exists(path): + return {} + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + + +def _save_store(ptype: str, store: dict) -> None: + path = _preset_path(ptype) + with open(path, "w", encoding="utf-8") as f: + json.dump(store, f, indent=2) + + +@app.route("/api/presets/", methods=["GET"]) +def presets_list(ptype: str): + """Return list of saved preset names and the last-used name.""" + if ptype not in _PRESET_TYPES: + return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400 + store = _load_store(ptype) + names = [k for k in store if not k.startswith("_")] + last = store.get("_last", None) + return jsonify({"success": True, "names": sorted(names), "last": last}) + + +@app.route("/api/presets//", methods=["GET"]) +def presets_load(ptype: str, name: str): + """Load a named preset and mark it as last-used.""" + if ptype not in _PRESET_TYPES: + return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400 + store = _load_store(ptype) + if name not in store: + return jsonify({"success": False, "error": f"Preset '{name}' not found"}), 404 + store["_last"] = name + _save_store(ptype, store) + return jsonify({"success": True, "name": name, "data": store[name]}) + + +@app.route("/api/presets//", methods=["POST"]) +def presets_save(ptype: str, name: str): + """Save (create or overwrite) a named preset and mark it as last-used.""" + if ptype not in _PRESET_TYPES: + return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400 + if not name or name.startswith("_"): + return jsonify({"success": False, "error": "Invalid preset name"}), 400 + data = request.get_json(force=True) + store = _load_store(ptype) + store[name] = data + store["_last"] = name + _save_store(ptype, store) + return jsonify({"success": True, "name": name}) + + +@app.route("/api/presets//", methods=["DELETE"]) +def presets_delete(ptype: str, name: str): + """Delete a named preset.""" + if ptype not in _PRESET_TYPES: + return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400 + store = _load_store(ptype) + if name not in store: + return jsonify({"success": False, "error": f"Preset '{name}' not found"}), 404 + del store[name] + if store.get("_last") == name: + remaining = [k for k in store if not k.startswith("_")] + store["_last"] = remaining[-1] if remaining else None + _save_store(ptype, store) + return jsonify({"success": True}) + + if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5000) diff --git a/draw_toroid.py b/draw_toroid.py index 9385223..3b967a0 100644 --- a/draw_toroid.py +++ b/draw_toroid.py @@ -78,6 +78,7 @@ def draw_toroid( output_path: str = "toroid.png", dpi: int = 180, fig_size_mm: float = 180.0, + actual_fill: float | None = None, ) -> None: """ Render a to-scale top-down cross-section of the transformer as a PNG. @@ -110,9 +111,10 @@ def draw_toroid( ax.set_xlabel("mm", fontsize=8) ax.set_ylabel("mm", fontsize=8) ax.tick_params(labelsize=7) + fill_str = f" fill={actual_fill*100:.1f}%" if actual_fill is not None else "" ax.set_title( f"Toroid cross-section " - f"ID={ID:.1f} mm OD={OD:.1f} mm H={core.height_mm:.1f} mm", + f"ID={ID:.1f} mm OD={OD:.1f} mm H={core.height_mm:.1f} mm{fill_str}", fontsize=9, pad=6, ) diff --git a/presets/constraints.json b/presets/constraints.json new file mode 100644 index 0000000..47de2cd --- /dev/null +++ b/presets/constraints.json @@ -0,0 +1,11 @@ +{ + "C1": { + "B_max_T": 1, + "Vp_max": 50, + "Vs_max": 90, + "Ip_max": 3, + "Is_max": 2, + "P_out_max_W": 100 + }, + "_last": "C1" +} \ No newline at end of file diff --git a/presets/core.json b/presets/core.json new file mode 100644 index 0000000..7614415 --- /dev/null +++ b/presets/core.json @@ -0,0 +1,11 @@ +{ + "FT-3KM F4424G": { + "ID_mm": 21.5, + "OD_mm": 46.5, + "height_mm": 22.8, + "fill_factor": 0.35, + "Ae_mm2": 142.5, + "Ve_mm3": 15219 + }, + "_last": "FT-3KM F4424G" +} \ No newline at end of file diff --git a/presets/sim.json b/presets/sim.json new file mode 100644 index 0000000..c87918d --- /dev/null +++ b/presets/sim.json @@ -0,0 +1,12 @@ +{ + "points": { + "frequencies": "256\n870\n3140\n8900\n12000\n16000\n22000\n33000\n45000", + "loads": "5\n10\n50\n100\n200\n600\n2000", + "target_power_W": 25, + "power_tol_pct": 2, + "Vp_min": 1, + "Vp_max": 50, + "Vp_steps": 100 + }, + "_last": "points" +} \ No newline at end of file diff --git a/presets/windings.json b/presets/windings.json new file mode 100644 index 0000000..f1259db --- /dev/null +++ b/presets/windings.json @@ -0,0 +1,35 @@ +{ + "Design 1": { + "windings": [ + { + "name": "primary", + "taps": [ + 0, + 25, + 50 + ], + "awg": [ + 22, + 22 + ] + }, + { + "name": "secondary", + "taps": [ + 0, + 100, + 50, + 50, + 50 + ], + "awg": [ + 22, + 22, + 22, + 22 + ] + } + ] + }, + "_last": "Design 1" +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 1ba730f..5ed5bb9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -285,6 +285,46 @@ } #plots-card { position: relative; } + /* ---- Preset bar ---- */ + .preset-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + background: #f1f5f9; + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; + } + .preset-bar select { + flex: 1; + min-width: 100px; + max-width: 200px; + font-size: 12px; + padding: 3px 6px; + } + .preset-bar input[type=text] { + flex: 1; + min-width: 80px; + max-width: 160px; + font-size: 12px; + padding: 3px 6px; + } + .btn-preset { + padding: 3px 10px; + font-size: 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + white-space: nowrap; + } + .btn-preset-load { background: #dbeafe; color: #1d4ed8; } + .btn-preset-load:hover { background: #bfdbfe; } + .btn-preset-save { background: #dcfce7; color: #166534; } + .btn-preset-save:hover { background: #bbf7d0; } + .btn-preset-delete { background: #fee2e2; color: #b91c1c; } + .btn-preset-delete:hover { background: #fca5a5; } + /* ---- Sweep results table ---- */ .sweep-table-wrap { overflow-x: auto; @@ -334,7 +374,7 @@ /* ---- Responsive: collapse to single column on narrow screens ---- */ @media (max-width: 860px) { .layout { grid-template-columns: 1fr; } - .plots-grid { grid-template-columns: 1fr; } + .plots-grid { grid-template-columns: 1fr !important; } } @@ -353,6 +393,13 @@
Core Parameters
+
+ + + + + +
@@ -372,6 +419,13 @@
Windings
+
+ + + + + +
@@ -397,6 +451,13 @@
Simulation
+
+ + + + + +
-
+
+
@@ -512,7 +593,8 @@ // State // ============================================================ let windingCount = 0; -let sweepData = null; // last successful sweep result +let sweepData = null; // last successful sweep result +let designSurfaceArea = null; // A_surface_m2 from last design run // ============================================================ // Winding builder @@ -656,8 +738,9 @@ async function runDesign() { return; } setMsg(msg, 'ok', 'Design complete.'); + designSurfaceArea = data.A_surface_m2 || null; showDrawing(data.drawing); - showDesignResults(data.windings); + showDesignResults(data.windings, data.fill_factor_target, data.fill_factor_actual, data.window_area_mm2, data.A_surface_m2); resContainer.style.display = 'block'; } catch(e) { setMsg(msg, 'error', 'Network error: ' + e.message); @@ -676,9 +759,29 @@ function showDrawing(b64) { img.style.display = 'block'; } -function showDesignResults(windings) { +function showDesignResults(windings, fillTarget, fillActual, windowArea, surfaceArea) { const cont = document.getElementById('design-results-tables'); cont.innerHTML = ''; + + // Fill factor summary row + if (fillActual != null) { + const pct = (fillActual * 100).toFixed(1); + const tgtPct = fillTarget != null ? (fillTarget * 100).toFixed(0) : '?'; + const over = fillActual > fillTarget * 1.005; + const color = over ? '#b91c1c' : '#166534'; + const surfStr = surfaceArea != null + ? ` |  Surface area: ${(surfaceArea * 1e4).toFixed(1)} cm²` : ''; + cont.innerHTML += ` +
+ Window area: ${windowArea != null ? windowArea.toFixed(1) : '?'} mm² +  |  + Target fill: ${tgtPct}% +  |  + Actual fill: ${pct}% + ${surfStr} +
`; + } + windings.forEach(w => { const feasStr = w.feasible ? 'OK' @@ -896,12 +999,39 @@ function updatePlots() { document.getElementById('plot-status').textContent = `${rows.length} points, ${rows.filter(r => r.met_target).length} met target`; - updateTable(rows); + // --- Temperature plot --- + const T_amb = parseFloat(document.getElementById('sim-tambient').value) || 25; + const h_conv = parseFloat(document.getElementById('sim-hconv').value) || 6; + const A_surf = designSurfaceArea; // m² from last design, null if not run yet + + const T_rise = rows.map(r => { + if (r.P_in_W == null || r.P_out_W == null || !A_surf) return null; + return (r.P_in_W - r.P_out_W) / (h_conv * A_surf); + }); + const T_copper = T_rise.map(dt => dt != null ? T_amb + dt : null); + + Plotly.react('plot-temp', [ + { x, y: T_rise, name: 'T rise (°C)', mode: 'lines+markers', type: 'scatter', + text: hoverTap, hovertemplate: 'R=%{x}Ω
ΔT=%{y:.1f}°C
%{text}' }, + { x, y: T_copper, name: 'T copper (°C)', mode: 'lines+markers', type: 'scatter', + line: {dash: 'dot'}, + text: hoverTap, hovertemplate: 'R=%{x}Ω
T=%{y:.1f}°C
%{text}' }, + ], { + title: { text: `Temperature @ ${freqLabel} (T_amb=${T_amb}°C, h=${h_conv} W/m²K)`, font: {size:12} }, + xaxis: { title: 'R load (Ω)', type: 'log' }, + yaxis: { title: 'Temperature (°C)' }, + margin: {t:40, b:42, l:54, r:10}, + legend: {font:{size:11}}, + height: plotH, + }, {responsive: true, displayModeBar: false}); + + updateTable(rows, T_rise, T_copper); } // ============================================================ // Results table (CSV-equivalent, all rows for selected freq) // ============================================================ +// All fields — used for CSV export only const _CSV_COLS = [ { key: 'freq_hz', label: 'freq (Hz)' }, { key: 'Z_load_R', label: 'R (Ω)' }, @@ -931,7 +1061,37 @@ const _CSV_COLS = [ { key: 'efficiency', label: 'eff' }, ]; -function fmtCell(key, val) { +// Columns shown in the on-screen table (subset + computed P_loss) +const _TABLE_COLS = [ + { key: 'freq_hz', label: 'freq (Hz)' }, + { key: 'Z_load_R', label: 'R (Ω)' }, + { key: 'Z_load_X', label: 'X (Ω)' }, + { key: 'met_target', label: 'met' }, + { key: 'primary_tap', label: 'P tap' }, + { key: 'secondary_tap', label: 'S tap' }, + { key: 'Vp_rms', label: 'Vp (V)' }, + { key: 'Np_eff', label: 'Np' }, + { key: 'Ns_eff', label: 'Ns' }, + { key: 'B_peak_T', label: 'B (T)' }, + { key: 'Vs_rms', label: 'Vs (V)' }, + { key: 'Ip_rms', label: 'Ip (A)' }, + { key: 'Is_rms', label: 'Is (A)' }, + { key: 'P_in_W', label: 'P_in (W)' }, + { key: 'P_out_W', label: 'P_out (W)' }, + { key: '_P_loss', label: 'P_loss (W)' }, // computed + { key: 'efficiency', label: 'eff' }, + { key: '_T_rise', label: 'ΔT (°C)' }, // computed from thermal model + { key: '_T_copper', label: 'T_cu (°C)' }, // computed +]; + +function fmtCell(key, val, row) { + if (key === '_P_loss') { + const pin = row && row.P_in_W != null ? row.P_in_W : null; + const pout = row && row.P_out_W != null ? row.P_out_W : null; + val = (pin != null && pout != null) ? pin - pout : null; + } + if (key === '_T_rise') { val = row ? row._T_rise : null; } + if (key === '_T_copper') { val = row ? row._T_copper : null; } if (val === null || val === undefined) return ''; if (key === 'met_target') { return val @@ -942,22 +1102,27 @@ function fmtCell(key, val) { return (val * 100).toFixed(2) + '%'; } if (typeof val === 'number') { - // integers: no decimals; floats: 2 decimal places return Number.isInteger(val) ? val : val.toFixed(2); } return val; } -function updateTable(rows) { +function updateTable(rows, T_rise, T_copper) { document.getElementById('table-card').style.display = 'block'; + // Attach computed thermal values directly onto each row object + rows.forEach((r, i) => { + r._T_rise = T_rise ? T_rise[i] : null; + r._T_copper = T_copper ? T_copper[i] : null; + }); + const thead = document.getElementById('sweep-table-head'); const tbody = document.getElementById('sweep-table-body'); - thead.innerHTML = '' + _CSV_COLS.map(c => `${c.label}`).join('') + ''; + thead.innerHTML = '' + _TABLE_COLS.map(c => `${c.label}`).join('') + ''; tbody.innerHTML = rows.map(r => - '' + _CSV_COLS.map(c => `${fmtCell(c.key, r[c.key])}`).join('') + '' + '' + _TABLE_COLS.map(c => `${fmtCell(c.key, r[c.key], r)}`).join('') + '' ).join(''); } @@ -1024,10 +1189,191 @@ function calcPlotHeight() { })(); // ============================================================ -// Initialise with default windings +// Preset system // ============================================================ -addWinding('primary', [25, 50], [22, 22]); -addWinding('secondary', [100, 50, 50, 50], [22, 22, 22, 26]); + +// Serialise current UI state for each preset type +function presetExtract(ptype) { + if (ptype === 'core') { + const d = { + ID_mm: parseFloat(document.getElementById('core-ID').value), + OD_mm: parseFloat(document.getElementById('core-OD').value), + height_mm: parseFloat(document.getElementById('core-H').value), + fill_factor: parseFloat(document.getElementById('core-fill').value), + }; + const ae = document.getElementById('core-Ae').value.trim(); + const ve = document.getElementById('core-Ve').value.trim(); + if (ae !== '') d.Ae_mm2 = parseFloat(ae); + if (ve !== '') d.Ve_mm3 = parseFloat(ve); + return d; + } + if (ptype === 'windings') { + const windings = []; + document.querySelectorAll('.winding-block').forEach(block => { + const name = block.querySelector('.winding-name').value.trim(); + const taps = [0], awg = []; + block.querySelectorAll('.seg-row').forEach(row => { + taps.push(parseInt(row.querySelector('.seg-turns').value)); + awg.push(parseInt(row.querySelector('.seg-awg').value)); + }); + windings.push({ name, taps, awg }); + }); + return { windings }; + } + if (ptype === 'sim') { + return { + frequencies: document.getElementById('sim-freqs').value, + loads: document.getElementById('sim-loads').value, + target_power_W: parseFloat(document.getElementById('sim-target').value), + power_tol_pct: parseFloat(document.getElementById('sim-tol').value), + Vp_min: parseFloat(document.getElementById('sim-vpmin').value), + Vp_max: parseFloat(document.getElementById('sim-vpmax').value), + Vp_steps: parseInt(document.getElementById('sim-vpsteps').value), + T_ambient_C: parseFloat(document.getElementById('sim-tambient').value), + h_conv: parseFloat(document.getElementById('sim-hconv').value), + }; + } + if (ptype === 'constraints') { + return { + B_max_T: parseFloat(document.getElementById('con-B').value), + Vp_max: parseFloat(document.getElementById('con-Vp').value), + Vs_max: parseFloat(document.getElementById('con-Vs').value), + Ip_max: parseFloat(document.getElementById('con-Ip').value), + Is_max: parseFloat(document.getElementById('con-Is').value), + P_out_max_W: parseFloat(document.getElementById('con-Pout').value), + }; + } +} + +// Apply loaded preset data back to the UI +function presetApply(ptype, data) { + if (ptype === 'core') { + document.getElementById('core-ID').value = data.ID_mm ?? ''; + document.getElementById('core-OD').value = data.OD_mm ?? ''; + document.getElementById('core-H').value = data.height_mm ?? ''; + document.getElementById('core-fill').value = data.fill_factor ?? 0.35; + document.getElementById('core-Ae').value = data.Ae_mm2 ?? ''; + document.getElementById('core-Ve').value = data.Ve_mm3 ?? ''; + } + if (ptype === 'windings') { + const container = document.getElementById('windings-container'); + container.innerHTML = ''; + windingCount = 0; + (data.windings || []).forEach(w => { + addWinding(w.name, w.taps.slice(1), w.awg); + }); + } + if (ptype === 'sim') { + if (data.frequencies !== undefined) document.getElementById('sim-freqs').value = data.frequencies; + if (data.loads !== undefined) document.getElementById('sim-loads').value = data.loads; + if (data.target_power_W !== undefined) document.getElementById('sim-target').value = data.target_power_W; + if (data.power_tol_pct !== undefined) document.getElementById('sim-tol').value = data.power_tol_pct; + if (data.Vp_min !== undefined) document.getElementById('sim-vpmin').value = data.Vp_min; + if (data.Vp_max !== undefined) document.getElementById('sim-vpmax').value = data.Vp_max; + if (data.Vp_steps !== undefined) document.getElementById('sim-vpsteps').value = data.Vp_steps; + if (data.T_ambient_C !== undefined) document.getElementById('sim-tambient').value = data.T_ambient_C; + if (data.h_conv !== undefined) document.getElementById('sim-hconv').value = data.h_conv; + } + if (ptype === 'constraints') { + if (data.B_max_T !== undefined) document.getElementById('con-B').value = data.B_max_T; + if (data.Vp_max !== undefined) document.getElementById('con-Vp').value = data.Vp_max; + if (data.Vs_max !== undefined) document.getElementById('con-Vs').value = data.Vs_max; + if (data.Ip_max !== undefined) document.getElementById('con-Ip').value = data.Ip_max; + if (data.Is_max !== undefined) document.getElementById('con-Is').value = data.Is_max; + if (data.P_out_max_W !== undefined) document.getElementById('con-Pout').value = data.P_out_max_W; + } +} + +// Populate a preset dropdown from server +async function presetRefresh(ptype) { + const sel = document.getElementById(`preset-sel-${ptype}`); + if (!sel) return null; + const resp = await fetch(`/api/presets/${ptype}`); + const data = await resp.json(); + if (!data.success) return null; + sel.innerHTML = data.names.length === 0 + ? '' + : data.names.map(n => ``).join(''); + // Sync name input to selected + const nameEl = document.getElementById(`preset-name-${ptype}`); + if (nameEl && data.last) nameEl.value = data.last; + return data; +} + +async function presetLoad(ptype) { + const sel = document.getElementById(`preset-sel-${ptype}`); + const name = sel ? sel.value : ''; + if (!name) return; + const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`); + const data = await resp.json(); + if (!data.success) { alert('Load failed: ' + data.error); return; } + presetApply(ptype, data.data); + // Sync name input + const nameEl = document.getElementById(`preset-name-${ptype}`); + if (nameEl) nameEl.value = name; +} + +async function presetSave(ptype) { + const nameEl = document.getElementById(`preset-name-${ptype}`); + const name = nameEl ? nameEl.value.trim() : ''; + if (!name) { alert('Enter a preset name first.'); return; } + const payload = presetExtract(ptype); + const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(payload), + }); + const data = await resp.json(); + if (!data.success) { alert('Save failed: ' + data.error); return; } + await presetRefresh(ptype); + // Select the newly saved name + const sel = document.getElementById(`preset-sel-${ptype}`); + if (sel) sel.value = name; +} + +async function presetDelete(ptype) { + const sel = document.getElementById(`preset-sel-${ptype}`); + const name = sel ? sel.value : ''; + if (!name || name === '(no saved presets)') return; + if (!confirm(`Delete preset "${name}"?`)) return; + const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, { method: 'DELETE' }); + const data = await resp.json(); + if (!data.success) { alert('Delete failed: ' + data.error); return; } + await presetRefresh(ptype); +} + +// On page load: refresh all dropdowns and auto-load last-used preset for each type +async function initPresets() { + for (const ptype of ['core', 'windings', 'sim', 'constraints']) { + const info = await presetRefresh(ptype); + if (info && info.last) { + // Auto-load last used + const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(info.last)}`); + const d = await resp.json(); + if (d.success) presetApply(ptype, d.data); + } + } +} + +// ============================================================ +// Initialise: load presets first; fall back to hardcoded defaults +// ============================================================ +(async () => { + // initPresets returns after applying last-used presets (if any) + const windingsInfo = await (async () => { + const r = await fetch('/api/presets/windings'); + const d = await r.json(); + return d.success ? d : null; + })(); + + // Only add default windings if no saved windings preset exists + if (!windingsInfo || !windingsInfo.last) { + addWinding('primary', [25, 50], [22, 22]); + addWinding('secondary', [100, 50, 50, 50], [22, 22, 22, 26]); + } + + await initPresets(); +})();