1381 lines
51 KiB
HTML
1381 lines
51 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Toroid Transformer Designer</title>
|
||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #f0f2f5;
|
||
color: #333;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ---- Top bar ---- */
|
||
.topbar {
|
||
background: linear-gradient(135deg, #1e3a5f 0%, #2d6a9f 100%);
|
||
color: white;
|
||
padding: 10px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||
}
|
||
.topbar h1 { font-size: 18px; font-weight: 600; }
|
||
.topbar .spacer { flex: 1; }
|
||
|
||
/* ---- Layout: two-column ---- */
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 380px 1fr;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
min-height: calc(100vh - 44px);
|
||
}
|
||
|
||
/* ---- Left column: panels stacked ---- */
|
||
.left-col { display: flex; flex-direction: column; gap: 12px; }
|
||
|
||
/* ---- Right column: drawing + plots ---- */
|
||
.right-col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
||
|
||
/* ---- Card ---- */
|
||
.card {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
|
||
overflow: hidden;
|
||
}
|
||
.card-header {
|
||
background: #1e3a5f;
|
||
color: white;
|
||
padding: 8px 14px;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
letter-spacing: 0.5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.card-body { padding: 14px; }
|
||
|
||
/* ---- Form elements ---- */
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.form-row.three { grid-template-columns: 1fr 1fr 1fr; }
|
||
.form-row.four { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
||
.form-row.one { grid-template-columns: 1fr; }
|
||
|
||
label.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
font-size: 12px;
|
||
color: #555;
|
||
}
|
||
label.field span { font-weight: 500; }
|
||
|
||
input[type=number], input[type=text], select {
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
padding: 5px 8px;
|
||
font-size: 13px;
|
||
width: 100%;
|
||
background: white;
|
||
}
|
||
input[type=number]:focus, input[type=text]:focus, select:focus {
|
||
outline: none;
|
||
border-color: #2d6a9f;
|
||
box-shadow: 0 0 0 2px rgba(45,106,159,0.2);
|
||
}
|
||
|
||
/* ---- Winding builder ---- */
|
||
.winding-block {
|
||
border: 1px solid #dde;
|
||
border-radius: 6px;
|
||
padding: 10px;
|
||
margin-bottom: 10px;
|
||
background: #fafbff;
|
||
position: relative;
|
||
}
|
||
.winding-block-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.winding-block-header input[type=text] {
|
||
flex: 1;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
.btn-sm {
|
||
padding: 3px 10px;
|
||
font-size: 12px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
}
|
||
.btn-remove { background: #fee2e2; color: #b91c1c; }
|
||
.btn-remove:hover { background: #fca5a5; }
|
||
.btn-add-seg { background: #dbeafe; color: #1d4ed8; }
|
||
.btn-add-seg:hover { background: #bfdbfe; }
|
||
|
||
.seg-row {
|
||
display: grid;
|
||
grid-template-columns: 60px 1fr 28px;
|
||
gap: 6px;
|
||
align-items: end;
|
||
margin-bottom: 6px;
|
||
}
|
||
.seg-row label.field span { white-space: nowrap; }
|
||
|
||
.btn-del-seg {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
height: 28px;
|
||
width: 28px;
|
||
line-height: 1;
|
||
}
|
||
.btn-del-seg:hover { background: #fca5a5; }
|
||
|
||
/* ---- Button row ---- */
|
||
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
|
||
.btn {
|
||
padding: 8px 20px;
|
||
border: none;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #1e3a5f, #2d6a9f);
|
||
color: white;
|
||
box-shadow: 0 2px 8px rgba(29,78,137,0.3);
|
||
}
|
||
.btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
|
||
.btn-secondary {
|
||
background: #e5e7eb;
|
||
color: #374151;
|
||
}
|
||
.btn-secondary:hover:not(:disabled) { background: #d1d5db; }
|
||
.btn-add-winding {
|
||
background: #ecfdf5;
|
||
color: #065f46;
|
||
border: 1px solid #6ee7b7;
|
||
}
|
||
.btn-add-winding:hover { background: #d1fae5; }
|
||
|
||
/* ---- Frequencies / loads textarea ---- */
|
||
textarea {
|
||
width: 100%;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
padding: 6px 8px;
|
||
font-size: 12px;
|
||
font-family: monospace;
|
||
resize: vertical;
|
||
min-height: 60px;
|
||
}
|
||
textarea:focus {
|
||
outline: none;
|
||
border-color: #2d6a9f;
|
||
box-shadow: 0 0 0 2px rgba(45,106,159,0.2);
|
||
}
|
||
|
||
/* ---- Status / error messages ---- */
|
||
.msg {
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
display: none;
|
||
}
|
||
.msg.visible { display: block; }
|
||
.msg-error { background: #fee2e2; color: #b91c1c; border: 1px solid #fca5a5; }
|
||
.msg-info { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
|
||
.msg-ok { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
|
||
|
||
/* ---- Design results table ---- */
|
||
.design-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 12px;
|
||
margin-top: 8px;
|
||
}
|
||
.design-table th {
|
||
background: #f1f5f9;
|
||
padding: 5px 8px;
|
||
text-align: left;
|
||
border-bottom: 2px solid #e2e8f0;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
}
|
||
.design-table td {
|
||
padding: 5px 8px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
.design-table tr:last-child td { border-bottom: none; }
|
||
.badge-ok { color: #166534; font-weight: 600; }
|
||
.badge-bad { color: #b91c1c; font-weight: 600; }
|
||
|
||
/* ---- Drawing ---- */
|
||
#drawing-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 200px;
|
||
background: #f8fafc;
|
||
border-radius: 6px;
|
||
color: #94a3b8;
|
||
font-size: 13px;
|
||
}
|
||
#drawing-img { max-width: 100%; border-radius: 4px; display: none; }
|
||
|
||
/* ---- Plots ---- */
|
||
.plot-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 0 14px 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.plot-toolbar label { font-size: 12px; font-weight: 500; color: #555; }
|
||
.plot-toolbar select { width: auto; }
|
||
|
||
.plots-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 8px;
|
||
padding: 0 14px 14px;
|
||
}
|
||
.plot-box { min-height: 300px; }
|
||
|
||
/* ---- Resizable plots card ---- */
|
||
#plots-card {
|
||
resize: vertical;
|
||
overflow: auto;
|
||
min-height: 360px;
|
||
}
|
||
/* Show a subtle resize hint in the bottom-right corner */
|
||
#plots-card::after {
|
||
content: '⠿';
|
||
position: absolute;
|
||
bottom: 4px;
|
||
right: 6px;
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
pointer-events: none;
|
||
line-height: 1;
|
||
}
|
||
#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;
|
||
padding: 0 14px 14px;
|
||
}
|
||
.sweep-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
}
|
||
.sweep-table th {
|
||
background: #1e3a5f;
|
||
color: white;
|
||
padding: 5px 8px;
|
||
text-align: right;
|
||
font-weight: 500;
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
.sweep-table th:first-child { text-align: left; }
|
||
.sweep-table td {
|
||
padding: 4px 8px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
text-align: right;
|
||
color: #374151;
|
||
}
|
||
.sweep-table td:first-child { text-align: left; }
|
||
.sweep-table tr:nth-child(even) td { background: #f8fafc; }
|
||
.sweep-table tr:hover td { background: #e0f2fe; }
|
||
.sweep-table .cell-ok { color: #166534; font-weight: 600; }
|
||
.sweep-table .cell-bad { color: #b91c1c; }
|
||
.sweep-table .cell-null { color: #94a3b8; }
|
||
|
||
/* ---- Spinner ---- */
|
||
.spinner {
|
||
display: inline-block;
|
||
width: 14px; height: 14px;
|
||
border: 2px solid rgba(255,255,255,0.3);
|
||
border-top-color: white;
|
||
border-radius: 50%;
|
||
animation: spin 0.6s linear infinite;
|
||
vertical-align: middle;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* ---- Responsive: collapse to single column on narrow screens ---- */
|
||
@media (max-width: 860px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
.plots-grid { grid-template-columns: 1fr !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<h1>Toroid Transformer Designer</h1>
|
||
<div class="spacer"></div>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
|
||
<!-- ===================== LEFT COLUMN ===================== -->
|
||
<div class="left-col">
|
||
|
||
<!-- Core Parameters -->
|
||
<div class="card">
|
||
<div class="card-header">Core Parameters</div>
|
||
<div class="preset-bar">
|
||
<select id="preset-sel-core" title="Saved cores"></select>
|
||
<button class="btn-preset btn-preset-load" onclick="presetLoad('core')">Load</button>
|
||
<input type="text" id="preset-name-core" placeholder="preset name">
|
||
<button class="btn-preset btn-preset-save" onclick="presetSave('core')">Save</button>
|
||
<button class="btn-preset btn-preset-delete" onclick="presetDelete('core')">Delete</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="form-row three">
|
||
<label class="field"><span>ID (mm)</span><input type="number" id="core-ID" value="21.5" min="1" step="0.1"></label>
|
||
<label class="field"><span>OD (mm)</span><input type="number" id="core-OD" value="46.5" min="2" step="0.1"></label>
|
||
<label class="field"><span>Height (mm)</span><input type="number" id="core-H" value="22.8" min="1" step="0.1"></label>
|
||
</div>
|
||
<div class="form-row two">
|
||
<label class="field"><span>Ae (mm²) optional</span><input type="number" id="core-Ae" value="142.5" min="0" step="0.1" placeholder="geometric default"></label>
|
||
<label class="field"><span>Ve (mm³) optional</span><input type="number" id="core-Ve" value="15219" min="0" step="1" placeholder="Ae × Le"></label>
|
||
</div>
|
||
<div class="form-row one" style="margin-top:4px">
|
||
<label class="field"><span>Fill factor</span><input type="number" id="core-fill" value="0.35" min="0.1" max="0.9" step="0.01"></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Windings -->
|
||
<div class="card">
|
||
<div class="card-header">Windings</div>
|
||
<div class="preset-bar">
|
||
<select id="preset-sel-windings" title="Saved windings"></select>
|
||
<button class="btn-preset btn-preset-load" onclick="presetLoad('windings')">Load</button>
|
||
<input type="text" id="preset-name-windings" placeholder="preset name">
|
||
<button class="btn-preset btn-preset-save" onclick="presetSave('windings')">Save</button>
|
||
<button class="btn-preset btn-preset-delete" onclick="presetDelete('windings')">Delete</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="windings-container"></div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-add-winding" onclick="addWinding()">+ Add Winding</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Design button + results -->
|
||
<div class="card">
|
||
<div class="card-header">Design</div>
|
||
<div class="card-body">
|
||
<div class="btn-row" style="margin-bottom:10px">
|
||
<button class="btn btn-primary" id="btn-design" onclick="runDesign()">Design Transformer</button>
|
||
</div>
|
||
<div id="design-msg" class="msg"></div>
|
||
<div id="design-results-container" style="display:none">
|
||
<div id="design-results-tables"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Simulation Parameters -->
|
||
<div class="card">
|
||
<div class="card-header">Simulation</div>
|
||
<div class="preset-bar">
|
||
<select id="preset-sel-sim" title="Saved sim settings"></select>
|
||
<button class="btn-preset btn-preset-load" onclick="presetLoad('sim')">Load</button>
|
||
<input type="text" id="preset-name-sim" placeholder="preset name">
|
||
<button class="btn-preset btn-preset-save" onclick="presetSave('sim')">Save</button>
|
||
<button class="btn-preset btn-preset-delete" onclick="presetDelete('sim')">Delete</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="form-row one">
|
||
<label class="field">
|
||
<span>Frequencies (Hz) — one per line or comma-separated</span>
|
||
<textarea id="sim-freqs" rows="3">256
|
||
870
|
||
3140
|
||
8900</textarea>
|
||
</label>
|
||
</div>
|
||
<div class="form-row one">
|
||
<label class="field">
|
||
<span>Loads [R, X] (ohms) — one pair per line: R or R,X</span>
|
||
<textarea id="sim-loads" rows="4">10
|
||
50
|
||
100
|
||
200
|
||
600
|
||
2000</textarea>
|
||
</label>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="field"><span>Target power (W)</span><input type="number" id="sim-target" value="25" min="0.1" step="0.1"></label>
|
||
<label class="field"><span>Power tolerance (%)</span><input type="number" id="sim-tol" value="2" min="0.1" step="0.1"></label>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="field"><span>Vp min (V)</span><input type="number" id="sim-vpmin" value="1" min="0.1" step="0.5"></label>
|
||
<label class="field"><span>Vp max (V)</span><input type="number" id="sim-vpmax" value="50" min="1" step="1"></label>
|
||
</div>
|
||
<div class="form-row one">
|
||
<label class="field"><span>Vp steps</span><input type="number" id="sim-vpsteps" value="100" min="10" max="500" step="10"></label>
|
||
</div>
|
||
|
||
<details style="margin: 8px 0">
|
||
<summary style="cursor:pointer; font-size:12px; color:#2d6a9f; font-weight:500">Constraints (expand)</summary>
|
||
<div class="preset-bar" style="margin:6px 0; padding:4px 0; background:none; border:none">
|
||
<select id="preset-sel-constraints" title="Saved constraints"></select>
|
||
<button class="btn-preset btn-preset-load" onclick="presetLoad('constraints')">Load</button>
|
||
<input type="text" id="preset-name-constraints" placeholder="preset name">
|
||
<button class="btn-preset btn-preset-save" onclick="presetSave('constraints')">Save</button>
|
||
<button class="btn-preset btn-preset-delete" onclick="presetDelete('constraints')">Delete</button>
|
||
</div>
|
||
<div style="margin-top:4px">
|
||
<div class="form-row">
|
||
<label class="field"><span>B max (T)</span><input type="number" id="con-B" value="0.3" min="0.01" step="0.01"></label>
|
||
<label class="field"><span>Vp max (V)</span><input type="number" id="con-Vp" value="50" min="1" step="1"></label>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="field"><span>Vs max (V)</span><input type="number" id="con-Vs" value="120" min="1" step="1"></label>
|
||
<label class="field"><span>Ip max (A)</span><input type="number" id="con-Ip" value="3" min="0.01" step="0.1"></label>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="field"><span>Is max (A)</span><input type="number" id="con-Is" value="2" min="0.01" step="0.1"></label>
|
||
<label class="field"><span>P out max (W)</span><input type="number" id="con-Pout" value="100" min="0.1" step="1"></label>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<div class="form-row" style="margin-top:8px">
|
||
<label class="field">
|
||
<span>Ambient temp (°C)</span>
|
||
<input type="number" id="sim-tambient" value="25" min="-40" max="100" step="1">
|
||
</label>
|
||
<label class="field">
|
||
<span>Conv. coeff h (W/m²K)</span>
|
||
<input type="number" id="sim-hconv" value="6" min="1" max="100" step="0.5"
|
||
title="Still-air natural convection ≈ 5–10 W/m²K">
|
||
</label>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" id="btn-sim" onclick="runSweep()">Run Simulation</button>
|
||
</div>
|
||
<div id="sim-msg" class="msg" style="margin-top:8px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /left-col -->
|
||
|
||
|
||
<!-- ===================== RIGHT COLUMN ===================== -->
|
||
<div class="right-col">
|
||
|
||
<!-- Drawing -->
|
||
<div class="card">
|
||
<div class="card-header">Cross-section Drawing</div>
|
||
<div class="card-body" style="padding:10px">
|
||
<div id="drawing-container">
|
||
<span>Run Design to see drawing</span>
|
||
</div>
|
||
<img id="drawing-img" alt="Toroid cross-section">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Plots -->
|
||
<div class="card" id="plots-card" style="display:none">
|
||
<div class="card-header">Simulation Results</div>
|
||
<div class="plot-toolbar">
|
||
<label for="freq-select">Frequency:</label>
|
||
<select id="freq-select" onchange="updatePlots()"></select>
|
||
<span id="plot-status" style="font-size:12px;color:#64748b;margin-left:8px"></span>
|
||
</div>
|
||
<div class="plots-grid" style="grid-template-columns: 1fr 1fr;">
|
||
<div class="plot-box" id="plot-voltage"></div>
|
||
<div class="plot-box" id="plot-current"></div>
|
||
<div class="plot-box" id="plot-power"></div>
|
||
<div class="plot-box" id="plot-temp"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results table -->
|
||
<div class="card" id="table-card" style="display:none">
|
||
<div class="card-header" style="justify-content:space-between">
|
||
<span>Results Table</span>
|
||
<a id="csv-download" href="#" download="sweep_results.csv"
|
||
style="color:#93c5fd;font-size:12px;font-weight:500;text-decoration:none"
|
||
onclick="return triggerCsvDownload()">↓ Download CSV</a>
|
||
</div>
|
||
<div class="sweep-table-wrap">
|
||
<table class="sweep-table" id="sweep-table">
|
||
<thead id="sweep-table-head"></thead>
|
||
<tbody id="sweep-table-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /right-col -->
|
||
|
||
</div><!-- /layout -->
|
||
|
||
<script>
|
||
// ============================================================
|
||
// State
|
||
// ============================================================
|
||
let windingCount = 0;
|
||
let sweepData = null; // last successful sweep result
|
||
let designSurfaceArea = null; // A_surface_m2 from last design run
|
||
|
||
// ============================================================
|
||
// Winding builder
|
||
// ============================================================
|
||
function addWinding(name, taps, awgs) {
|
||
const container = document.getElementById('windings-container');
|
||
const id = windingCount++;
|
||
name = name || (id === 0 ? 'primary' : id === 1 ? 'secondary' : `winding${id+1}`);
|
||
taps = taps || [25];
|
||
awgs = awgs || [22];
|
||
|
||
const div = document.createElement('div');
|
||
div.className = 'winding-block';
|
||
div.dataset.id = id;
|
||
|
||
// Build segment rows HTML
|
||
let segHtml = '';
|
||
for (let i = 0; i < taps.length; i++) {
|
||
segHtml += segRowHtml(id, i, awgs[i] || 22, taps[i]);
|
||
}
|
||
|
||
div.innerHTML = `
|
||
<div class="winding-block-header">
|
||
<input type="text" class="winding-name" value="${name}" placeholder="name">
|
||
<button class="btn-sm btn-remove" onclick="removeWinding(${id})">Remove</button>
|
||
</div>
|
||
<div class="seg-list" id="segs-${id}">
|
||
${segHtml}
|
||
</div>
|
||
<button class="btn-sm btn-add-seg" onclick="addSegment(${id})" style="margin-top:4px">+ Add segment</button>
|
||
`;
|
||
container.appendChild(div);
|
||
}
|
||
|
||
function segRowHtml(wid, segIdx, awg, turns) {
|
||
const awgOptions = [20,21,22,23,24,25,26,27,28,29,30].map(a =>
|
||
`<option value="${a}" ${a == awg ? 'selected' : ''}>${a}</option>`
|
||
).join('');
|
||
return `
|
||
<div class="seg-row" data-seg="${segIdx}">
|
||
<label class="field"><span>AWG</span>
|
||
<select class="seg-awg">${awgOptions}</select>
|
||
</label>
|
||
<label class="field"><span>Turns (seg ${segIdx+1})</span>
|
||
<input type="number" class="seg-turns" value="${turns}" min="1" step="1">
|
||
</label>
|
||
<button class="btn-del-seg" onclick="removeSegment(this)" title="Remove segment">-</button>
|
||
</div>`;
|
||
}
|
||
|
||
function addSegment(wid) {
|
||
const list = document.getElementById(`segs-${wid}`);
|
||
const existing = list.querySelectorAll('.seg-row');
|
||
const newIdx = existing.length;
|
||
const div = document.createElement('div');
|
||
div.innerHTML = segRowHtml(wid, newIdx, 22, 50);
|
||
list.appendChild(div.firstElementChild);
|
||
renumberSegments(list);
|
||
}
|
||
|
||
function removeSegment(btn) {
|
||
const row = btn.closest('.seg-row');
|
||
const list = row.parentElement;
|
||
if (list.querySelectorAll('.seg-row').length <= 1) {
|
||
alert('Each winding must have at least one segment.');
|
||
return;
|
||
}
|
||
row.remove();
|
||
renumberSegments(list);
|
||
}
|
||
|
||
function renumberSegments(list) {
|
||
list.querySelectorAll('.seg-row').forEach((row, i) => {
|
||
row.dataset.seg = i;
|
||
const lbl = row.querySelector('.field:nth-child(2) span');
|
||
if (lbl) lbl.textContent = `Turns (seg ${i+1})`;
|
||
});
|
||
}
|
||
|
||
function removeWinding(id) {
|
||
const container = document.getElementById('windings-container');
|
||
if (container.querySelectorAll('.winding-block').length <= 1) {
|
||
alert('Need at least one winding.');
|
||
return;
|
||
}
|
||
container.querySelector(`.winding-block[data-id="${id}"]`).remove();
|
||
}
|
||
|
||
// ============================================================
|
||
// Build request payload from current UI state
|
||
// ============================================================
|
||
function buildPayload() {
|
||
const payload = {
|
||
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),
|
||
windings: [],
|
||
};
|
||
const aeVal = document.getElementById('core-Ae').value.trim();
|
||
const veVal = document.getElementById('core-Ve').value.trim();
|
||
if (aeVal !== '') payload.Ae_mm2 = parseFloat(aeVal);
|
||
if (veVal !== '') payload.Ve_mm3 = parseFloat(veVal);
|
||
|
||
document.querySelectorAll('.winding-block').forEach(block => {
|
||
const name = block.querySelector('.winding-name').value.trim();
|
||
const taps = [0];
|
||
const awg = [];
|
||
block.querySelectorAll('.seg-row').forEach(row => {
|
||
taps.push(parseInt(row.querySelector('.seg-turns').value));
|
||
awg.push(parseInt(row.querySelector('.seg-awg').value));
|
||
});
|
||
payload.windings.push({ name, taps, awg });
|
||
});
|
||
return payload;
|
||
}
|
||
|
||
// ============================================================
|
||
// Design
|
||
// ============================================================
|
||
async function runDesign() {
|
||
const btn = document.getElementById('btn-design');
|
||
const msg = document.getElementById('design-msg');
|
||
const resContainer = document.getElementById('design-results-container');
|
||
|
||
setMsg(msg, 'info', 'Designing...');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner"></span> Designing...';
|
||
resContainer.style.display = 'none';
|
||
|
||
try {
|
||
const payload = buildPayload();
|
||
const resp = await fetch('/api/design', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await resp.json();
|
||
if (!data.success) {
|
||
setMsg(msg, 'error', 'Error: ' + data.error);
|
||
return;
|
||
}
|
||
setMsg(msg, 'ok', 'Design complete.');
|
||
designSurfaceArea = data.A_surface_m2 || null;
|
||
showDrawing(data.drawing);
|
||
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);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Design Transformer';
|
||
}
|
||
}
|
||
|
||
function showDrawing(b64) {
|
||
const container = document.getElementById('drawing-container');
|
||
const img = document.getElementById('drawing-img');
|
||
container.innerHTML = '';
|
||
container.style.display = 'none';
|
||
img.src = b64;
|
||
img.style.display = 'block';
|
||
}
|
||
|
||
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: <strong>${(surfaceArea * 1e4).toFixed(1)} cm²</strong>` : '';
|
||
cont.innerHTML += `
|
||
<div style="margin-bottom:10px;padding:6px 10px;background:#f1f5f9;border-radius:6px;font-size:12px">
|
||
Window area: <strong>${windowArea != null ? windowArea.toFixed(1) : '?'} mm²</strong>
|
||
|
|
||
Target fill: <strong>${tgtPct}%</strong>
|
||
|
|
||
Actual fill: <strong style="color:${color}">${pct}%</strong>
|
||
${surfStr}
|
||
</div>`;
|
||
}
|
||
|
||
windings.forEach(w => {
|
||
const feasStr = w.feasible
|
||
? '<span class="badge-ok">OK</span>'
|
||
: '<span class="badge-bad">DOES NOT FIT</span>';
|
||
let html = `
|
||
<div style="margin-bottom:12px">
|
||
<strong>${w.name}</strong> — ${w.total_turns} turns total, ${w.total_wire_length_m.toFixed(2)} m,
|
||
${w.total_resistance_mohm.toFixed(1)} mΩ, ${w.total_weight_g.toFixed(2)} g — ${feasStr}
|
||
<table class="design-table" style="margin-top:6px">
|
||
<thead><tr>
|
||
<th>Tap</th><th>AWG</th><th>Turns</th>
|
||
<th>Wire (m)</th><th>R (mΩ)</th><th>Layers</th><th>Fit</th>
|
||
</tr></thead>
|
||
<tbody>`;
|
||
w.segments.forEach(seg => {
|
||
const layerSummary = seg.layers.map(lr =>
|
||
`L${lr.layer_index}: ${lr.turns_used}/${lr.turns_capacity}`
|
||
).join(', ');
|
||
const fitStr = seg.fits
|
||
? '<span class="badge-ok">OK</span>'
|
||
: '<span class="badge-bad">OVERFLOW</span>';
|
||
html += `<tr>
|
||
<td>${seg.tap_number}</td>
|
||
<td>${seg.awg}</td>
|
||
<td>${seg.turns}</td>
|
||
<td>${seg.wire_length_m.toFixed(3)}</td>
|
||
<td>${seg.resistance_mohm.toFixed(2)}</td>
|
||
<td style="font-size:11px">${layerSummary}</td>
|
||
<td>${fitStr}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table></div>';
|
||
cont.innerHTML += html;
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// Sweep (simulate)
|
||
// ============================================================
|
||
function parseFrequencies() {
|
||
const raw = document.getElementById('sim-freqs').value;
|
||
return raw.split(/[\n,]+/).map(s => s.trim()).filter(s => s !== '').map(Number).filter(v => v > 0);
|
||
}
|
||
|
||
function parseLoads() {
|
||
const raw = document.getElementById('sim-loads').value;
|
||
return raw.split('\n').map(s => s.trim()).filter(s => s !== '').map(line => {
|
||
const parts = line.split(',').map(Number);
|
||
return [parts[0] || 0, parts[1] || 0];
|
||
}).filter(p => p[0] > 0 || p[1] !== 0);
|
||
}
|
||
|
||
async function runSweep() {
|
||
const btn = document.getElementById('btn-sim');
|
||
const msg = document.getElementById('sim-msg');
|
||
const plotsCard = document.getElementById('plots-card');
|
||
|
||
setMsg(msg, 'info', 'Running simulation...');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner"></span> Simulating...';
|
||
|
||
try {
|
||
const corePayload = buildPayload();
|
||
const freqs = parseFrequencies();
|
||
const loads = parseLoads();
|
||
|
||
if (freqs.length === 0) { setMsg(msg, 'error', 'No valid frequencies.'); return; }
|
||
if (loads.length === 0) { setMsg(msg, 'error', 'No valid loads.'); return; }
|
||
|
||
const payload = Object.assign({}, corePayload, {
|
||
frequencies: freqs,
|
||
loads: loads,
|
||
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),
|
||
constraints: {
|
||
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),
|
||
},
|
||
});
|
||
|
||
const resp = await fetch('/api/sweep', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (!data.success) {
|
||
setMsg(msg, 'error', 'Error: ' + data.error);
|
||
return;
|
||
}
|
||
|
||
sweepData = data;
|
||
setMsg(msg, 'ok', `Done — ${data.rows.length} operating points computed.`);
|
||
|
||
// Populate frequency dropdown
|
||
const sel = document.getElementById('freq-select');
|
||
sel.innerHTML = '';
|
||
data.frequencies.forEach((f, i) => {
|
||
const opt = document.createElement('option');
|
||
opt.value = i;
|
||
opt.textContent = f >= 1000 ? `${(f/1000).toFixed(1)} kHz` : `${f} Hz`;
|
||
sel.appendChild(opt);
|
||
});
|
||
plotsCard.style.display = 'block';
|
||
updatePlots();
|
||
|
||
} catch(e) {
|
||
setMsg(msg, 'error', 'Network error: ' + e.message);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Run Simulation';
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Plots
|
||
// ============================================================
|
||
function updatePlots() {
|
||
if (!sweepData) return;
|
||
|
||
const freqIdx = parseInt(document.getElementById('freq-select').value);
|
||
const freq = sweepData.frequencies[freqIdx];
|
||
|
||
// Filter rows for this frequency, sort by load resistance
|
||
const rows = sweepData.rows
|
||
.filter(r => r.freq_hz === freq)
|
||
.sort((a, b) => a.Z_load_R - b.Z_load_R);
|
||
|
||
const freqLabel = freq >= 1000 ? `${(freq/1000).toFixed(1)} kHz` : `${freq} Hz`;
|
||
|
||
// Build single arrays — one point per load, using the optimizer's chosen taps
|
||
const x = rows.map(r => r.Z_load_R);
|
||
const Vs = rows.map(r => r.Vs_rms);
|
||
const Is = rows.map(r => r.Is_rms);
|
||
const Ip = rows.map(r => r.Ip_rms);
|
||
const Pout = rows.map(r => r.P_out_W);
|
||
const Pin = rows.map(r => r.P_in_W);
|
||
const Pcu = rows.map(r => r.P_cu_W);
|
||
const Pcore= rows.map(r => r.P_core_W);
|
||
const eff = rows.map(r => r.efficiency != null ? r.efficiency * 100 : null);
|
||
const Vp = rows.map(r => r.Vp_rms);
|
||
|
||
// Hover text: show the selected tap combination and Vp
|
||
const hoverTap = rows.map(r =>
|
||
r.primary_tap != null
|
||
? `P${r.primary_tap}/S${r.secondary_tap}<br>Vp=${r.Vp_rms != null ? r.Vp_rms.toFixed(2) : '?'}V`
|
||
: 'no solution'
|
||
);
|
||
|
||
const plotH = calcPlotHeight();
|
||
|
||
// --- Voltage plot ---
|
||
Plotly.react('plot-voltage', [
|
||
{ x, y: Vp, name: 'Vp', mode: 'lines+markers', type: 'scatter',
|
||
line: {dash: 'dot'},
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>Vp=%{y:.2f}V<br>%{text}<extra></extra>' },
|
||
{ x, y: Vs, name: 'Vs', mode: 'lines+markers', type: 'scatter',
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>Vs=%{y:.2f}V<br>%{text}<extra></extra>' },
|
||
], {
|
||
title: { text: `Voltage @ ${freqLabel}`, font: {size:13} },
|
||
xaxis: { title: 'R load (Ω)', type: 'log' },
|
||
yaxis: { title: 'V rms' },
|
||
margin: {t:36, b:42, l:54, r:10},
|
||
legend: {font:{size:11}},
|
||
height: plotH,
|
||
}, {responsive: true, displayModeBar: false});
|
||
|
||
// --- Current plot ---
|
||
Plotly.react('plot-current', [
|
||
{ x, y: Is, name: 'Is', mode: 'lines+markers', type: 'scatter',
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>Is=%{y:.4f}A<br>%{text}<extra></extra>' },
|
||
{ x, y: Ip, name: 'Ip', mode: 'lines+markers', type: 'scatter',
|
||
line: {dash: 'dot'},
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>Ip=%{y:.4f}A<br>%{text}<extra></extra>' },
|
||
], {
|
||
title: { text: `Current @ ${freqLabel}`, font: {size:13} },
|
||
xaxis: { title: 'R load (Ω)', type: 'log' },
|
||
yaxis: { title: 'I rms (A)' },
|
||
margin: {t:36, b:42, l:54, r:10},
|
||
legend: {font:{size:11}},
|
||
height: plotH,
|
||
}, {responsive: true, displayModeBar: false});
|
||
|
||
// --- Power plot ---
|
||
Plotly.react('plot-power', [
|
||
{ x, y: Pout, name: 'P out', mode: 'lines+markers', type: 'scatter',
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>P_out=%{y:.3f}W<br>%{text}<extra></extra>' },
|
||
{ x, y: Pin, name: 'P in', mode: 'lines+markers', type: 'scatter',
|
||
line: {dash:'dash'},
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>P_in=%{y:.3f}W<br>%{text}<extra></extra>' },
|
||
{ x, y: Pcu, name: 'P cu', mode: 'lines+markers', type: 'scatter',
|
||
line: {dash:'dot'},
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>P_cu=%{y:.3f}W<br>%{text}<extra></extra>' },
|
||
{ x, y: eff, name: 'Eff %', mode: 'lines+markers', type: 'scatter',
|
||
yaxis: 'y2', line: {dash:'dot'},
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>Eff=%{y:.1f}%<br>%{text}<extra></extra>' },
|
||
], {
|
||
title: { text: `Power & Efficiency @ ${freqLabel}`, font: {size:13} },
|
||
xaxis: { title: 'R load (Ω)', type: 'log' },
|
||
yaxis: { title: 'Power (W)' },
|
||
yaxis2: { title: 'Efficiency (%)', overlaying: 'y', side: 'right', range: [0, 105] },
|
||
margin: {t:36, b:42, l:54, r:55},
|
||
legend: {font:{size:11}},
|
||
height: plotH,
|
||
}, {responsive: true, displayModeBar: false});
|
||
|
||
document.getElementById('plot-status').textContent =
|
||
`${rows.length} points, ${rows.filter(r => r.met_target).length} met target`;
|
||
|
||
// --- 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}Ω<br>ΔT=%{y:.1f}°C<br>%{text}<extra></extra>' },
|
||
{ x, y: T_copper, name: 'T copper (°C)', mode: 'lines+markers', type: 'scatter',
|
||
line: {dash: 'dot'},
|
||
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>T=%{y:.1f}°C<br>%{text}<extra></extra>' },
|
||
], {
|
||
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 (Ω)' },
|
||
{ key: 'Z_load_X', label: 'X (Ω)' },
|
||
{ key: 'target_power_W', label: 'target (W)' },
|
||
{ key: 'met_target', label: 'met' },
|
||
{ key: 'power_error_pct', label: 'err (%)' },
|
||
{ 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: 'turns_ratio', label: 'ratio' },
|
||
{ 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: 'load_phase_deg', label: 'phase (°)' },
|
||
{ key: 'Rp_ohm', label: 'Rp (Ω)' },
|
||
{ key: 'Rs_ohm', label: 'Rs (Ω)' },
|
||
{ key: 'P_out_W', label: 'P_out (W)' },
|
||
{ key: 'P_cu_W', label: 'P_cu (W)' },
|
||
{ key: 'P_cu_primary_W', label: 'P_cu_p (W)' },
|
||
{ key: 'P_cu_secondary_W', label: 'P_cu_s (W)' },
|
||
{ key: 'P_core_W', label: 'P_core (W)' },
|
||
{ key: 'P_in_W', label: 'P_in (W)' },
|
||
{ key: 'efficiency', label: 'eff' },
|
||
];
|
||
|
||
// 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 '<span class="cell-null">—</span>';
|
||
if (key === 'met_target') {
|
||
return val
|
||
? '<span class="cell-ok">YES</span>'
|
||
: '<span class="cell-bad">NO</span>';
|
||
}
|
||
if (key === 'efficiency' && typeof val === 'number') {
|
||
return (val * 100).toFixed(2) + '%';
|
||
}
|
||
if (typeof val === 'number') {
|
||
return Number.isInteger(val) ? val : val.toFixed(2);
|
||
}
|
||
return val;
|
||
}
|
||
|
||
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 = '<tr>' + _TABLE_COLS.map(c => `<th>${c.label}</th>`).join('') + '</tr>';
|
||
|
||
tbody.innerHTML = rows.map(r =>
|
||
'<tr>' + _TABLE_COLS.map(c => `<td>${fmtCell(c.key, r[c.key], r)}</td>`).join('') + '</tr>'
|
||
).join('');
|
||
}
|
||
|
||
function triggerCsvDownload() {
|
||
if (!sweepData) return false;
|
||
const cols = _CSV_COLS.map(c => c.key);
|
||
const header = _CSV_COLS.map(c => c.label).join(',');
|
||
const lines = sweepData.rows.map(r =>
|
||
cols.map(k => {
|
||
const v = r[k];
|
||
if (v === null || v === undefined) return '';
|
||
if (k === 'efficiency' && typeof v === 'number') return (v * 100).toFixed(4);
|
||
if (typeof v === 'number') return Number.isInteger(v) ? v : v.toFixed(4);
|
||
return v;
|
||
}).join(',')
|
||
);
|
||
const csv = [header, ...lines].join('\r\n');
|
||
const blob = new Blob([csv], {type: 'text/csv'});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.getElementById('csv-download');
|
||
a.href = url;
|
||
// Let the browser follow the href naturally for download
|
||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||
return true; // allow default href navigation (triggers download)
|
||
}
|
||
|
||
// ============================================================
|
||
// Helpers
|
||
// ============================================================
|
||
function setMsg(el, type, text) {
|
||
el.className = 'msg visible msg-' + type;
|
||
el.textContent = text;
|
||
}
|
||
|
||
// Calculate per-plot height.
|
||
// Uses an explicit override when set (e.g. from a manual resize), otherwise 300px default.
|
||
let _plotHeightOverride = null;
|
||
|
||
function calcPlotHeight() {
|
||
return _plotHeightOverride !== null ? _plotHeightOverride : 300;
|
||
}
|
||
|
||
// Re-render plots when the user manually drags the card taller.
|
||
// We detect manual resize by watching card.style.height (set by the browser's
|
||
// resize handle), NOT clientHeight (which changes when plot content grows).
|
||
(function setupResizeObserver() {
|
||
const card = document.getElementById('plots-card');
|
||
if (!card || typeof ResizeObserver === 'undefined') return;
|
||
let rafId = null;
|
||
const ro = new ResizeObserver(() => {
|
||
// Only act when the inline style height is set (i.e. user dragged the handle)
|
||
const styleH = parseInt(card.style.height);
|
||
if (!styleH || isNaN(styleH)) return;
|
||
// Derive plot height from the dragged card height
|
||
const newPlotH = Math.max(220, styleH - 84);
|
||
if (newPlotH === _plotHeightOverride) return;
|
||
_plotHeightOverride = newPlotH;
|
||
if (rafId) cancelAnimationFrame(rafId);
|
||
rafId = requestAnimationFrame(() => {
|
||
if (sweepData) updatePlots();
|
||
});
|
||
});
|
||
ro.observe(card);
|
||
})();
|
||
|
||
// ============================================================
|
||
// Preset system
|
||
// ============================================================
|
||
|
||
// 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
|
||
? '<option value="">(no saved presets)</option>'
|
||
: data.names.map(n => `<option value="${n}"${n === data.last ? ' selected' : ''}>${n}</option>`).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();
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|