this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add shunt junction elements and MoC dual-render mode (Steps 5/6/7)

- index.html: extended load control (R/C/L/open/short), junction list div,
tau_d input, renamed TDR canvas to Probe
- style.css: junction-item, load-row, tau-d-row, unit-lbl styles
- app.js: buildSegmentInputs (N Z0 inputs + N-1 junction rows), syncModelFromInputs
(builds model.blocks with interleaved tl/shunt blocks, normalises pF/nH to SI),
getOrComputeSim (JSON-key cache), dual render() path (bounce-series vs MoC sim),
renamed el.tdr -> el.probe throughout
- render.js: drawShuntC/L/R/Short + drawShuntElement, updated drawCircuit to accept
segments/terminal/junctions/wavefronts params and draw shunt symbols in gaps between
T-line boxes, C/L terminal variants; added drawPlotSim (total V(z,t) + V+/V- per
segment), drawProbeSim (source node V vs time); renamed drawTDR -> drawProbe,
drawTDRSim -> drawProbeSim

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+651 -93
+265 -75
app.js
··· 4 4 // Depends on: TLUtils (utils.js), TLPhysics (physics.js), TLRender (render.js) 5 5 (() => { 6 6 const { clamp, fmt, resizeCanvasToCSS } = TLUtils; 7 - const { computeWaveParams, buildBounceSeries, computeDynamicState } = TLPhysics; 8 - const { getTheme, drawCircuit, drawPlot, drawTDR, ensurePlotCanvasHeight } = TLRender; 7 + const { 8 + computeWaveParams, buildBounceSeries, computeDynamicState, 9 + simulateTimeDomain, voltageAt, 10 + } = TLPhysics; 11 + const { getTheme, drawCircuit, drawPlot, drawPlotSim, drawProbe, drawProbeSim, ensurePlotCanvasHeight } = TLRender; 9 12 10 13 // ---- DOM references ---- 11 14 const el = { 12 - tdr: document.getElementById("tdr"), 15 + probe: document.getElementById("probe"), 13 16 circuit: document.getElementById("circuit"), 14 17 plot: document.getElementById("plot"), 15 18 startBtn: document.getElementById("startBtn"), ··· 17 20 resetBtn: document.getElementById("resetBtn"), 18 21 Vg: document.getElementById("Vg"), 19 22 Rg: document.getElementById("Rg"), 20 - RL: document.getElementById("RL"), 21 - RLOpen: document.getElementById("RLOpen"), 23 + loadType: document.getElementById("loadType"), 24 + loadValue: document.getElementById("loadValue"), 25 + loadUnit: document.getElementById("loadUnit"), 22 26 segCount: document.getElementById("segCount"), 23 27 segZ0List: document.getElementById("segZ0List"), 24 - secPerTau: document.getElementById("secPerTau"), 25 - secPerTauRead: document.getElementById("secPerTauRead"), 28 + junctionList: document.getElementById("junctionList"), 29 + tauD: document.getElementById("tauD"), 30 + secPerTau: document.getElementById("secPerTau"), 31 + secPerTauRead: document.getElementById("secPerTauRead"), 26 32 reflectTol: document.getElementById("reflectTol"), 27 33 tRead: document.getElementById("tRead"), 28 34 gLRead: document.getElementById("gLRead"), ··· 36 42 37 43 // ---- model (physics parameters) ---- 38 44 const model = { 39 - Vg: 5, 40 - Rg: 20, 41 - segments: [{ Z0: 50 }], // array of N equal-length segments, each with a Z0 42 - RL: 30, 43 - secPerTau: 2.5, 44 - reflectTol: 1, 45 - riseTimeTr: 0, 46 - riseShape: "step", // "step" | "linear" | "erf" 45 + Vg: 5, 46 + Rg: 20, 47 + segments: [{ Z0: 50 }], // backward-compat for bounce series 48 + RL: 30, 49 + blocks: [], // set by syncModelFromInputs 50 + terminal: { type: 'R', value: 30 }, 51 + tau_d: 1e-9, 52 + secPerTau: 2.5, 53 + reflectTol: 1, 54 + riseTimeTr: 0, 55 + riseShape: "step", 47 56 }; 48 57 49 - // ---- segment input management ---- 50 - let segZ0Inputs = []; 58 + // ---- segment + junction input management ---- 59 + let segZ0Inputs = []; 60 + let junctionInputs = []; // [{typeEl, valueEl}] for N-1 internal junctions 61 + 62 + const UNIT_FOR_TYPE = { R: 'Ω', C: 'pF', L: 'nH', short: '', none: '' }; 51 63 52 64 function buildSegmentInputs(n) { 53 - // Preserve existing values where possible. 54 - const prev = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); 55 - el.segZ0List.innerHTML = ""; 56 - segZ0Inputs = []; 65 + // Preserve existing Z0 values. 66 + const prevZ0 = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); 67 + const prevJ = junctionInputs.map((j) => ({ type: j.typeEl.value, val: j.valueEl.value })); 68 + el.segZ0List.innerHTML = ""; 69 + el.junctionList.innerHTML = ""; 70 + segZ0Inputs = []; 71 + junctionInputs = []; 72 + 57 73 for (let i = 0; i < n; i++) { 58 - const val = (prev[i] != null) ? prev[i] : 50; 59 - const inp = document.createElement("input"); 60 - inp.type = "number"; 61 - inp.min = "0.1"; 62 - inp.step = "0.1"; 63 - inp.value = val; 64 - inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; 65 - inp.addEventListener("input", () => { if (!running) render(); }); 66 - inp.addEventListener("change", () => { if (!running) render(); }); 74 + // Z0 input for segment i 75 + const z0val = (prevZ0[i] != null) ? prevZ0[i] : 50; 76 + const inp = document.createElement("input"); 77 + inp.type = "number"; 78 + inp.min = "0.1"; 79 + inp.step = "0.1"; 80 + inp.value = z0val; 81 + inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; 82 + inp.addEventListener("input", rerender); 83 + inp.addEventListener("change", rerender); 67 84 el.segZ0List.appendChild(inp); 68 85 segZ0Inputs.push(inp); 86 + 87 + // Junction element between segment i and i+1 88 + if (i < n - 1) { 89 + const p = prevJ[i] || { type: 'none', val: '100' }; 90 + const row = document.createElement("div"); 91 + row.className = "junction-item"; 92 + 93 + const lbl = document.createElement("span"); 94 + lbl.textContent = `${i + 1}\u21C4${i + 2}`; 95 + 96 + const typeEl = document.createElement("select"); 97 + for (const t of ['none', 'R', 'C', 'L', 'short']) { 98 + const opt = document.createElement("option"); 99 + opt.value = t; 100 + opt.textContent = t === 'none' ? '— none —' : t; 101 + if (t === p.type) opt.selected = true; 102 + typeEl.appendChild(opt); 103 + } 104 + 105 + const valueEl = document.createElement("input"); 106 + valueEl.type = "number"; 107 + valueEl.min = "0"; 108 + valueEl.step = "0.1"; 109 + valueEl.value = p.val; 110 + valueEl.style.display = (p.type === 'none' || p.type === 'short') ? 'none' : ''; 111 + 112 + const unitEl = document.createElement("span"); 113 + unitEl.textContent = UNIT_FOR_TYPE[p.type] || ''; 114 + unitEl.style.minWidth = "24px"; 115 + 116 + typeEl.addEventListener("change", () => { 117 + const t = typeEl.value; 118 + valueEl.style.display = (t === 'none' || t === 'short') ? 'none' : ''; 119 + unitEl.textContent = UNIT_FOR_TYPE[t] || ''; 120 + rerender(); 121 + }); 122 + valueEl.addEventListener("input", rerender); 123 + valueEl.addEventListener("change", rerender); 124 + 125 + row.appendChild(lbl); 126 + row.appendChild(typeEl); 127 + row.appendChild(valueEl); 128 + row.appendChild(unitEl); 129 + el.junctionList.appendChild(row); 130 + junctionInputs.push({ typeEl, valueEl }); 131 + } 69 132 } 70 133 } 71 134 72 135 // Initialise with N=1. 73 136 buildSegmentInputs(1); 74 137 75 - // ---- open-circuit toggle state ---- 76 - let rlIsOpen = false; 138 + // ---- load type control ---- 139 + el.loadType.addEventListener("change", () => { 140 + const t = el.loadType.value; 141 + el.loadValue.disabled = (t === 'open' || t === 'short'); 142 + el.loadUnit.textContent = UNIT_FOR_TYPE[t] || ''; 143 + rerender(); 144 + }); 145 + el.loadValue.addEventListener("input", rerender); 146 + el.loadValue.addEventListener("change", rerender); 77 147 78 148 // ---- animation state ---- 79 149 let running = false; ··· 85 155 let mathjaxTypesetDone = false; 86 156 let theme = getTheme(); 87 157 158 + // ---- simulation cache ---- 159 + // cachedSim is recomputed whenever the model changes (detected by JSON key). 160 + let cachedSim = null; 161 + let cachedSimKey = ''; 162 + 163 + // Returns true if any junction or terminal has reactive (C or L) elements, 164 + // meaning we must use simulateTimeDomain for correct physics. 165 + function needsMoCSim() { 166 + if (!model.blocks) return false; 167 + for (const blk of model.blocks) { 168 + if (blk.type === 'C' || blk.type === 'L') return true; 169 + } 170 + if (model.terminal.type === 'C' || model.terminal.type === 'L') return true; 171 + return false; 172 + } 173 + 174 + // Returns true if the model has ANY shunt element (R, C, L, or short) at a 175 + // junction — i.e., anything beyond the default "just T-line segments" topology. 176 + function hasJunctionElements() { 177 + if (!model.blocks) return false; 178 + for (const blk of model.blocks) { 179 + if (blk.type !== 'tl') return true; 180 + } 181 + return false; 182 + } 183 + 184 + function getOrComputeSim() { 185 + const key = JSON.stringify(model.blocks) + JSON.stringify(model.terminal) 186 + + model.tau_d + model.riseTimeTr + model.riseShape + model.Vg + model.Rg; 187 + if (key !== cachedSimKey) { 188 + const tEnd = Math.max(16, timeHorizon + 2); 189 + cachedSim = simulateTimeDomain(model, { tEnd, oversample: 16 }); 190 + cachedSimKey = key; 191 + } 192 + return cachedSim; 193 + } 194 + 88 195 // ---- model sync ---- 89 196 function syncModelFromInputs() { 90 - model.Vg = parseFloat(el.Vg.value); 91 - model.Rg = parseFloat(el.Rg.value); 92 - model.RL = rlIsOpen ? Infinity : parseFloat(el.RL.value); 93 - model.secPerTau = parseFloat(el.secPerTau.value); 94 - model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 95 - model.riseShape = el.riseMode.value === "step" ? "step" 96 - : el.riseMode.value === "linear" ? "linear" : "erf"; 97 - model.riseTimeTr = model.riseShape !== "step" 197 + model.Vg = parseFloat(el.Vg.value); 198 + model.Rg = parseFloat(el.Rg.value); 199 + model.secPerTau = parseFloat(el.secPerTau.value); 200 + model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 201 + model.riseShape = el.riseMode.value === "step" ? "step" 202 + : el.riseMode.value === "linear" ? "linear" : "erf"; 203 + model.riseTimeTr = model.riseShape !== "step" 98 204 ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) 99 205 : 0; 100 - model.segments = segZ0Inputs.map((inp) => ({ 101 - Z0: Math.max(0.1, parseFloat(inp.value) || 50), 102 - })); 206 + model.tau_d = Math.max(1e-12, parseFloat(el.tauD.value) || 1) * 1e-9; 207 + 208 + const segsZ0 = segZ0Inputs.map((inp) => Math.max(0.1, parseFloat(inp.value) || 50)); 209 + const N = segsZ0.length; 210 + const tau = 1 / N; // normalized tau per segment (each segment = 1/N of total τ_d) 211 + 212 + // Build model.blocks: interleave TL segments with junction shunt elements. 213 + const blocks = []; 214 + for (let i = 0; i < N; i++) { 215 + blocks.push({ type: 'tl', Z0: segsZ0[i], tau }); 216 + if (i < N - 1 && junctionInputs[i]) { 217 + const jt = junctionInputs[i].typeEl.value; 218 + const jvRaw = parseFloat(junctionInputs[i].valueEl.value) || 0; 219 + if (jt !== 'none') { 220 + // Convert from display units to SI. 221 + // R: Ω (direct), C: pF → F (* 1e-12), L: nH → H (* 1e-9) 222 + const jv = jt === 'C' ? jvRaw * 1e-12 : jt === 'L' ? jvRaw * 1e-9 : jvRaw; 223 + blocks.push({ type: jt, value: jv }); 224 + } 225 + } 226 + } 227 + model.blocks = blocks; 228 + 229 + // Build model.terminal from load type/value. 230 + const lt = el.loadType.value; 231 + const lvRaw = parseFloat(el.loadValue.value) || 0; 232 + if (lt === 'open') { 233 + model.terminal = { type: 'open' }; 234 + model.RL = Infinity; 235 + } else if (lt === 'short') { 236 + model.terminal = { type: 'short' }; 237 + model.RL = 0; 238 + } else if (lt === 'R') { 239 + const rv = Math.max(0.001, lvRaw); 240 + model.terminal = { type: 'R', value: rv }; 241 + model.RL = rv; 242 + } else if (lt === 'C') { 243 + model.terminal = { type: 'C', value: lvRaw * 1e-12 }; 244 + model.RL = Infinity; // cap looks open at DC initially 245 + } else if (lt === 'L') { 246 + model.terminal = { type: 'L', value: lvRaw * 1e-9 }; 247 + model.RL = 0; // inductor shorts at DC 248 + } 249 + 250 + // Backward-compat: model.segments used by buildBounceSeries. 251 + model.segments = segsZ0.map((Z0) => ({ Z0 })); 103 252 } 104 253 105 - // ---- derived-value readout panel ---- 254 + // ---- derived-value readout panel (bounce-series mode only) ---- 106 255 function updateDerivedDisplays(model, waves, bounce) { 107 256 const N = model.segments.length; 108 257 const gS = bounce.gSEffective; ··· 124 273 lines.push(`V<sub>1</sub> = ${fmt(waves.V1, 6)} V`); 125 274 126 275 if (N === 1) { 127 - // Single-segment detail: show V2, V3, suppression reason. 128 276 const V2calc = waves.gL * waves.V1; 129 277 const V3calc = gS * V2calc; 130 278 const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; ··· 151 299 }).join(""); 152 300 } 153 301 302 + // Build junctions array for drawCircuit from junctionInputs. 303 + function getJunctionsForDraw() { 304 + return junctionInputs.map((j) => ({ type: j.typeEl.value })); 305 + } 306 + 154 307 // ---- render frame ---- 155 308 function render() { 156 309 syncModelFromInputs(); 157 - const waves = computeWaveParams(model); 158 - const bounce = buildBounceSeries(model, waves); 159 - const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTr, model.riseShape); 160 - timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 161 310 162 311 el.secPerTauRead.textContent = fmt(model.secPerTau, 1); 163 312 el.tRead.textContent = fmt(tNorm, 3); 164 - el.gLRead.textContent = fmt(waves.gL, 3); 165 - el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 166 - el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 167 - updateDerivedDisplays(model, waves, bounce); 168 313 169 314 if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { 170 315 mathjaxTypesetDone = true; 171 316 window.MathJax.typesetPromise(); 172 317 } 173 318 174 - const d = resizeCanvasToCSS(el.tdr); 175 - drawTDR(d.ctx, d.w, d.h, tNorm, bounce, model.riseTimeTr, model.riseShape, timeHorizon, theme); 176 - 177 - const c = resizeCanvasToCSS(el.circuit); 178 - drawCircuit(c.ctx, c.w, c.h, tNorm, dyn, theme, model.segments, model.RL); 179 - 180 - ensurePlotCanvasHeight(el.plot, 2); 181 - const p = resizeCanvasToCSS(el.plot); 182 - drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTr, model.riseShape, model.segments); 183 - 184 319 if (!hasStarted) { 185 320 el.pauseBtn.textContent = "Pause"; 186 321 } else { 187 322 el.pauseBtn.textContent = running ? "Pause" : "Resume"; 188 323 } 189 324 el.pauseBtn.disabled = !hasStarted; 325 + 326 + const junctions = getJunctionsForDraw(); 327 + const simMode = hasJunctionElements() || needsMoCSim(); 328 + 329 + if (simMode) { 330 + // ---- MoC simulation mode ---- 331 + const sim = getOrComputeSim(); 332 + const tIdx = Math.min(Math.round(tNorm / sim.dt), sim.nSteps - 1); 333 + const VS = sim.nodeV[0][tIdx]; 334 + const VL = sim.nodeV[sim.nodeV.length - 1][tIdx]; 335 + 336 + el.gLRead.textContent = '—'; 337 + el.vsRead.textContent = `${fmt(VS, 3)} V`; 338 + el.vlRead.textContent = `${fmt(VL, 3)} V`; 339 + el.derivedValues.innerHTML = '<div>MoC simulation mode — junction elements active</div>'; 340 + el.waveValues.innerHTML = ''; 341 + 342 + // Compute timeHorizon from sim 343 + timeHorizon = Math.min(sim.nSteps * sim.dt, tNorm + 1); 344 + timeHorizon = Math.max(timeHorizon, 2.2); 345 + 346 + // Probe — draw VS(t) from sim.nodeV[0] 347 + const d = resizeCanvasToCSS(el.probe); 348 + drawProbeSim(d.ctx, d.w, d.h, tNorm, sim, timeHorizon, theme); 349 + 350 + // Circuit — show shunt element symbols 351 + const c = resizeCanvasToCSS(el.circuit); 352 + drawCircuit(c.ctx, c.w, c.h, tNorm, theme, model.segments, model.terminal, junctions, []); 353 + 354 + // Plot — V(z,t) from sim 355 + ensurePlotCanvasHeight(el.plot, 2); 356 + const p = resizeCanvasToCSS(el.plot); 357 + drawPlotSim(p.ctx, p.w, p.h, tIdx, sim, theme); 358 + 359 + } else { 360 + // ---- Bounce-series mode (pure resistive, no junction elements) ---- 361 + const waves = computeWaveParams(model); 362 + const bounce = buildBounceSeries(model, waves); 363 + const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTr, model.riseShape); 364 + timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 365 + 366 + el.gLRead.textContent = fmt(waves.gL, 3); 367 + el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 368 + el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 369 + updateDerivedDisplays(model, waves, bounce); 370 + 371 + const wavefronts = dyn.activeWaves.map((wf) => ({ z: wf.front, dir: wf.dir })); 372 + 373 + const d = resizeCanvasToCSS(el.probe); 374 + drawProbe(d.ctx, d.w, d.h, tNorm, bounce, model.riseTimeTr, model.riseShape, timeHorizon, theme); 375 + 376 + const c = resizeCanvasToCSS(el.circuit); 377 + drawCircuit(c.ctx, c.w, c.h, tNorm, theme, model.segments, model.terminal, junctions, wavefronts); 378 + 379 + ensurePlotCanvasHeight(el.plot, 2); 380 + const p = resizeCanvasToCSS(el.plot); 381 + drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTr, model.riseShape, model.segments); 382 + } 190 383 } 191 384 192 385 // ---- animation loop ---- ··· 240 433 render(); 241 434 } 242 435 243 - // ---- event wiring ---- 244 - el.RLOpen.addEventListener("click", () => { 245 - rlIsOpen = !rlIsOpen; 246 - el.RL.disabled = rlIsOpen; 247 - el.RLOpen.classList.toggle("active", rlIsOpen); 436 + function rerender() { 248 437 if (!running) render(); 249 - }); 438 + } 250 439 440 + // ---- event wiring ---- 251 441 el.startBtn.addEventListener("click", start); 252 442 el.pauseBtn.addEventListener("click", pause); 253 443 el.resetBtn.addEventListener("click", reset); 254 444 255 - for (const inp of [el.Vg, el.Rg, el.RL, el.secPerTau, el.reflectTol, el.riseTau]) { 256 - inp.addEventListener("input", () => { if (!running) render(); }); 257 - inp.addEventListener("change", () => { if (!running) render(); }); 445 + for (const inp of [el.Vg, el.Rg, el.secPerTau, el.reflectTol, el.riseTau, el.tauD]) { 446 + inp.addEventListener("input", rerender); 447 + inp.addEventListener("change", rerender); 258 448 } 259 449 260 450 el.segCount.addEventListener("input", () => { 261 451 const n = Math.max(1, Math.min(10, parseInt(el.segCount.value) || 1)); 262 452 el.segCount.value = n; 263 453 buildSegmentInputs(n); 264 - if (!running) render(); 454 + rerender(); 265 455 }); 266 456 267 457 el.riseMode.addEventListener("change", () => { 268 458 el.riseTau.disabled = el.riseMode.value === "step"; 269 - if (!running) render(); 459 + rerender(); 270 460 }); 271 461 272 - window.addEventListener("resize", () => { theme = getTheme(); render(); }); 462 + window.addEventListener("resize", () => { theme = getTheme(); render(); }); 273 463 274 464 // Initial render 275 465 render();
+21 -3
index.html
··· 25 25 <div class="cfg-inputs-row"> 26 26 <div class="cfg-row"><span>V<sub>g</sub> =</span><input id="Vg" type="number" step="0.1" value="5.0" /><span>V</span></div> 27 27 <div class="cfg-row"><span>R<sub>g</sub> =</span><input id="Rg" type="number" step="0.1" value="20.0" /><span>(Ω)</span></div> 28 - <div class="cfg-row"><span>R<sub>L</sub> =</span><input id="RL" type="number" step="0.1" value="30.0" /><span>(Ω)</span><button id="RLOpen" type="button" title="Open circuit (R_L = ∞)">∞</button></div> 28 + <div class="cfg-row load-row"> 29 + <span>Load</span> 30 + <select id="loadType"> 31 + <option value="open">Open (∞)</option> 32 + <option value="R" selected>R (Ω)</option> 33 + <option value="short">Short (0)</option> 34 + <option value="C">C (pF)</option> 35 + <option value="L">L (nH)</option> 36 + </select> 37 + <input id="loadValue" type="number" step="0.1" value="30.0" /> 38 + <span id="loadUnit">Ω</span> 39 + </div> 29 40 </div> 30 41 <div class="cfg-panels-row"> 31 42 <div class="cfg-extra"> ··· 38 49 <label><span class="lbl">Z<sub>0</sub> per segment (Ω)</span> 39 50 <div id="segZ0List" class="seg-z0-list"></div> 40 51 </label> 52 + <div id="junctionList" class="junction-list"></div> 41 53 </div> 42 54 <div class="cfg-extra"> 55 + <label><span class="lbl">τ<sub>d</sub> (real-time delay for C/L values)</span> 56 + <div class="tau-d-row"> 57 + <input id="tauD" type="number" min="0.001" step="0.1" value="1" /> 58 + <span class="unit-lbl">ns</span> 59 + </div> 60 + </label> 43 61 <label><span class="lbl">Reflection termination tolerance (% of |V<sub>1</sub>|)</span> 44 62 <input id="reflectTol" type="number" min="0" step="0.1" value="1" /> 45 63 </label> ··· 65 83 <button id="resetBtn">Reset</button> 66 84 </div> 67 85 68 - <!-- TDR canvas (above circuit) --> 69 - <canvas id="tdr" width="600" height="300" style="width:600px;margin:0 auto;" aria-label="TDR waveform — voltage at detection point vs time"></canvas> 86 + <!-- Probe canvas: voltage at source node vs time --> 87 + <canvas id="probe" width="600" height="300" style="width:600px;margin:0 auto;" aria-label="Probe — voltage at source node vs time"></canvas> 70 88 71 89 <div class="divider"></div> 72 90
+348 -15
render.js
··· 126 126 label(ctx, name, x + 5, (yTop + yBot) / 2 - 6, theme.muted); 127 127 } 128 128 129 + // ---- shunt element symbols ---- 130 + // All draw vertically from (x, yA) down to (x, yB), connecting signal to GND. 131 + 132 + function drawShuntR(ctx, x, yA, yB, theme) { 133 + ctx.strokeStyle = theme.ink; 134 + ctx.lineWidth = 2; 135 + drawResistor(ctx, x, yA, x, yB, 4, 7, theme); 136 + } 137 + 138 + function drawShuntC(ctx, x, yA, yB, theme) { 139 + const midY = (yA + yB) / 2; 140 + const plateHalfW = 13; 141 + const plateGap = 9; 142 + ctx.strokeStyle = theme.ink; 143 + ctx.lineWidth = 2; 144 + line(ctx, x, yA, x, midY - plateGap / 2); 145 + line(ctx, x - plateHalfW, midY - plateGap / 2, x + plateHalfW, midY - plateGap / 2); 146 + line(ctx, x - plateHalfW, midY + plateGap / 2, x + plateHalfW, midY + plateGap / 2); 147 + line(ctx, x, midY + plateGap / 2, x, yB); 148 + } 149 + 150 + function drawShuntL(ctx, x, yA, yB, theme) { 151 + const nBumps = 4; 152 + const bumpR = 7; 153 + const bumpSpan = nBumps * bumpR * 2; 154 + const midY = (yA + yB) / 2; 155 + const bumpTop = midY - bumpSpan / 2; 156 + ctx.strokeStyle = theme.ink; 157 + ctx.lineWidth = 2; 158 + line(ctx, x, yA, x, bumpTop); 159 + for (let i = 0; i < nBumps; i++) { 160 + const cy = bumpTop + (i + 0.5) * bumpR * 2; 161 + ctx.beginPath(); 162 + ctx.arc(x, cy, bumpR, Math.PI, 0); 163 + ctx.stroke(); 164 + } 165 + line(ctx, x, bumpTop + bumpSpan, x, yB); 166 + } 167 + 168 + function drawShuntShort(ctx, x, yA, yB, theme) { 169 + ctx.strokeStyle = theme.ink; 170 + ctx.lineWidth = 3; 171 + line(ctx, x, yA, x, yB); 172 + ctx.lineWidth = 2; 173 + } 174 + 175 + // Draw a shunt element of given type between (x, yA) and (x, yB), with dot at node. 176 + function drawShuntElement(ctx, x, yA, yB, type, theme) { 177 + circleFill(ctx, x, yA, 4, theme.ink); 178 + if (type === 'R') drawShuntR(ctx, x, yA, yB, theme); 179 + else if (type === 'C') drawShuntC(ctx, x, yA, yB, theme); 180 + else if (type === 'L') drawShuntL(ctx, x, yA, yB, theme); 181 + else if (type === 'short') drawShuntShort(ctx, x, yA, yB, theme); 182 + } 183 + 129 184 // ---- circuit canvas ---- 130 - // segments = model.segments = [{Z0}, …] (N entries, equal-length) 131 - // RL = model.RL — pass Infinity for open circuit 132 - function drawCircuit(ctx, w, h, tn, dyn, theme, segments, RL) { 133 - const rlIsOpen = !isFinite(RL); 134 - const rlIsShort = RL === 0; 185 + // segments = [{Z0}, …] (N entries) 186 + // terminal = {type: 'R'|'C'|'L'|'open'|'short', value?} 187 + // junctions = [{type: 'none'|'R'|'C'|'L'|'short'}] length N-1 (internal junctions) 188 + // wavefronts = [{z, dir}] (optional; positions in [0,1] from active waves) 189 + function drawCircuit(ctx, w, h, tn, theme, segments, terminal, junctions, wavefronts) { 190 + terminal = terminal || { type: 'open' }; 191 + junctions = junctions || []; 192 + wavefronts = wavefronts || []; 193 + const rlIsOpen = terminal.type === 'open'; 194 + const rlIsShort = terminal.type === 'short'; 135 195 ctx.clearRect(0, 0, w, h); 136 196 ctx.fillStyle = theme.panel; 137 197 ctx.fillRect(0, 0, w, h); ··· 197 257 label(ctx, lbl, lblX, yTop - 22, theme.muted); 198 258 } 199 259 200 - // Load — resistor, short circuit wire, or open-circuit terminals 260 + // Shunt junction elements (between segments) 261 + const shuntYA = yTop + 16; // just below top box 262 + const shuntYB = yBot - 16; // just above bottom box 263 + for (let i = 0; i < N - 1; i++) { 264 + const junc = junctions[i]; 265 + if (!junc || junc.type === 'none') continue; 266 + const xj = xTL0 + (i + 1) * segPxW; 267 + drawShuntElement(ctx, xj, shuntYA, shuntYB, junc.type, theme); 268 + } 269 + 270 + // Load — resistor, capacitor, inductor, short, or open 201 271 const rlTop = yTop + 22, rlBot = yBot - 22; 202 272 ctx.strokeStyle = theme.ink; 203 273 ctx.lineWidth = 2; ··· 208 278 ctx.lineWidth = 3; 209 279 line(ctx, xLoad, yTop, xLoad, yBot); 210 280 ctx.lineWidth = 2; 211 - } else { 281 + } else if (terminal.type === 'R') { 212 282 line(ctx, xLoad, yTop, xLoad, rlTop); 213 283 drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 5, 8, theme); 214 284 line(ctx, xLoad, rlBot, xLoad, yBot); 215 285 label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 286 + } else if (terminal.type === 'C') { 287 + line(ctx, xLoad, yTop, xLoad, rlTop); 288 + drawShuntC(ctx, xLoad, rlTop, rlBot, theme); 289 + line(ctx, xLoad, rlBot, xLoad, yBot); 290 + label(ctx, "CL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 291 + } else if (terminal.type === 'L') { 292 + line(ctx, xLoad, yTop, xLoad, rlTop); 293 + drawShuntL(ctx, xLoad, rlTop, rlBot, theme); 294 + line(ctx, xLoad, rlBot, xLoad, yBot); 295 + label(ctx, "LL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 216 296 } 217 297 218 298 // Voltage probes ··· 225 305 circleFill(ctx, xRight, yBot, 3.5, theme.ink); 226 306 } 227 307 228 - // TDR detection point — green dot on top wire between switch and T-line input 308 + // Probe point — dot on top wire between switch and T-line input 229 309 const xDetect = Math.round((xSwitch + 18 + xTL0) / 2); 230 310 circleFill(ctx, xDetect, yTop, 5.5, theme.tdr); 231 311 ctx.fillStyle = theme.tdr; ··· 235 315 // Wavefront markers (dashed vertical lines inside T-line boxes) 236 316 const wfY0 = yTop - 16; 237 317 const wfY1 = yBot + 16; 238 - for (const wf of dyn.activeWaves) { 239 - const xw = xTL0 + wf.front * tlW; 318 + for (const wf of wavefronts) { 319 + const xw = xTL0 + wf.z * tlW; 240 320 ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2; 241 321 ctx.lineWidth = 3; 242 322 ctx.setLineDash([9, 7]); ··· 499 579 ctx.fillText("\u2113", xPlot1 - 4, h - 6); 500 580 } 501 581 502 - // ---- TDR canvas ---- 503 - // Draws VS(t) — voltage at the detection point (z=0, between Rg and T-line) versus time. 582 + // ---- plot canvas (MoC sim mode) ---- 583 + // Draws total V(z,t) and per-segment V⁺/V⁻ from simulateTimeDomain output. 584 + // sim: return value of simulateTimeDomain 585 + // tIdx: time step index (tNorm / sim.dt, clamped to [0, nSteps-1]) 586 + function drawPlotSim(ctx, w, h, tIdx, sim, theme) { 587 + ctx.clearRect(0, 0, w, h); 588 + ctx.fillStyle = theme.panel; 589 + ctx.fillRect(0, 0, w, h); 590 + 591 + const circuitPad = 18; 592 + const xPlot0 = circuitPad + 240; 593 + const xPlot1 = w - circuitPad - 210; 594 + const plotW = xPlot1 - xPlot0; 595 + const xOfZ = (z) => xPlot0 + z * plotW; 596 + 597 + const { voltageAt } = TLPhysics; 598 + const N = sim.segs.length; 599 + const step = Math.max(0, Math.min(tIdx, sim.nSteps - 1)); 600 + 601 + // Sample total voltage at evenly-spaced z values 602 + const NPTS = 400; 603 + const vArr = []; 604 + for (let i = 0; i <= NPTS; i++) { 605 + vArr.push(voltageAt(i / NPTS, step, sim)); 606 + } 607 + 608 + // Per-segment V⁺ and V⁻ (sampled from vPlusHist / vMinusHist via voltageAt internals) 609 + // We read them directly from the history arrays using the same delay logic as voltageAt. 610 + const totalTau = sim.segs.reduce((s, seg) => s + seg.tau, 0); 611 + 612 + const vScale = Math.max(1e-6, ...vArr.map(Math.abs)); 613 + const vLo = -1.15 * vScale, vHi = 1.15 * vScale; 614 + 615 + ctx.fillStyle = theme.muted; 616 + ctx.font = "12px ui-sans-serif, system-ui"; 617 + ctx.fillText("Voltage along the T-line (MoC simulation)", 12, 14); 618 + 619 + const panelH = PLOT_PANEL_H; 620 + const top0 = PLOT_PAD_T; 621 + const bot0 = top0 + panelH; 622 + const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 623 + 624 + // Panel frame helper (inline) 625 + function drawFrame(top, bot, yOfV, labelText) { 626 + const step_ = niceStep(vHi - vLo, 5); 627 + const dec = step_ >= 1 ? 0 : step_ >= 0.1 ? 1 : 2; 628 + const iTick0 = Math.ceil(vLo / step_); 629 + ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; 630 + for (let i = iTick0; ; i++) { 631 + const v = i * step_; 632 + if (v > vHi + step_ * 0.01) break; 633 + const yv = yOfV(v); 634 + if (yv < top - 1 || yv > bot + 1) continue; 635 + line(ctx, xPlot0, yv, xPlot1, yv); 636 + } 637 + ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; 638 + line(ctx, xPlot0, top, xPlot1, top); 639 + line(ctx, xPlot0, bot, xPlot1, bot); 640 + line(ctx, xPlot0, top, xPlot0, bot); 641 + line(ctx, xPlot1, top, xPlot1, bot); 642 + const yZero = yOfV(0); 643 + if (yZero >= top - 1 && yZero <= bot + 1) { 644 + ctx.strokeStyle = theme.muted; ctx.lineWidth = 1.2; 645 + line(ctx, xPlot0, yZero, xPlot1, yZero); 646 + } 647 + ctx.textAlign = "right"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 648 + for (let i = iTick0; ; i++) { 649 + const v = i * step_; 650 + if (v > vHi + step_ * 0.01) break; 651 + const yv = yOfV(v); 652 + if (yv < top - 1 || yv > bot + 1) continue; 653 + ctx.strokeStyle = theme.muted; ctx.lineWidth = 1; 654 + line(ctx, xPlot0 - 4, yv, xPlot0, yv); 655 + ctx.fillStyle = theme.muted; 656 + ctx.fillText(v.toFixed(dec), xPlot0 - 7, yv + 4); 657 + } 658 + ctx.textAlign = "left"; 659 + ctx.fillStyle = theme.muted; ctx.font = "12px ui-sans-serif, system-ui"; 660 + ctx.fillText(labelText, xPlot0 + 8, top + 14); 661 + ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4); 662 + // Segment boundary tick marks 663 + if (N > 1) { 664 + let cumZ = 0; 665 + ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; ctx.setLineDash([3, 4]); 666 + for (let i = 0; i < N - 1; i++) { 667 + cumZ += sim.segs[i].tau / totalTau; 668 + const xb = xOfZ(cumZ); 669 + line(ctx, xb, top, xb, bot); 670 + } 671 + ctx.setLineDash([]); 672 + } 673 + } 674 + 675 + drawFrame(top0, bot0, y0, "V(z, t) — total"); 676 + 677 + // Draw total V(z,t) 678 + ctx.strokeStyle = theme.ok; ctx.lineWidth = 2.4; 679 + ctx.beginPath(); 680 + for (let i = 0; i <= NPTS; i++) { 681 + const x = xOfZ(i / NPTS), y = y0(vArr[i]); 682 + if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); 683 + } 684 + ctx.stroke(); 685 + 686 + // Panel 2: V⁺ and V⁻ per segment 687 + const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP; 688 + const bot1 = top1 + panelH; 689 + const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 690 + drawFrame(top1, bot1, y1, "V\u207A (forward) and V\u207B (backward) per segment"); 691 + 692 + let cumZ = 0; 693 + for (let i = 0; i < N; i++) { 694 + const segFrac = sim.segs[i].tau / totalTau; 695 + const D = sim.nDelay[i]; 696 + const zL = cumZ, zR = cumZ + segFrac; 697 + 698 + // Sample V⁺ and V⁻ across this segment 699 + const NSEG = Math.max(2, Math.round(NPTS * segFrac)); 700 + ctx.strokeStyle = theme.accent; ctx.lineWidth = 1.8; 701 + ctx.beginPath(); 702 + for (let j = 0; j <= NSEG; j++) { 703 + const f = j / NSEG; 704 + const kFwd = Math.round(f * D); 705 + const tF = step - kFwd; 706 + const vP = (tF >= 0) ? sim.vPlusHist[i][tF] : 0; 707 + const x = xOfZ(zL + f * segFrac), y = y1(vP); 708 + if (j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); 709 + } 710 + ctx.stroke(); 711 + 712 + ctx.strokeStyle = theme.accent2; ctx.lineWidth = 1.8; 713 + ctx.beginPath(); 714 + for (let j = 0; j <= NSEG; j++) { 715 + const f = j / NSEG; 716 + const kBwd = Math.round((1 - f) * D); 717 + const tB = step - kBwd; 718 + const vM = (tB >= 0) ? sim.vMinusHist[i][tB] : 0; 719 + const x = xOfZ(zL + f * segFrac), y = y1(vM); 720 + if (j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); 721 + } 722 + ctx.stroke(); 723 + 724 + cumZ += segFrac; 725 + } 726 + 727 + // z-axis labels 728 + ctx.fillStyle = theme.muted; ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 729 + ctx.fillText("0", xPlot0 - 4, h - 6); 730 + ctx.fillText("\u2113", xPlot1 - 4, h - 6); 731 + } 732 + 733 + // ---- Probe canvas (MoC sim mode) ---- 734 + // Draws V_source(t) from sim.nodeV[0]. 735 + function drawProbeSim(ctx, w, h, tn, sim, timeHorizon_, theme_) { 736 + ctx.clearRect(0, 0, w, h); 737 + ctx.fillStyle = theme_.panel; 738 + ctx.fillRect(0, 0, w, h); 739 + 740 + const padL = 55, padR = 30, padT = 20, padB = 30; 741 + const plotW_ = w - padL - padR; 742 + const plotH_ = h - padT - padB; 743 + const tMax = timeHorizon_; 744 + const xOfT = (t) => padL + (t / tMax) * plotW_; 745 + 746 + const vArr = sim.nodeV[0]; 747 + let vMax = 0, vMin = 0; 748 + for (let k = 0; k < sim.nSteps; k++) { 749 + if (vArr[k] > vMax) vMax = vArr[k]; 750 + if (vArr[k] < vMin) vMin = vArr[k]; 751 + } 752 + vMax = Math.max(vMax, 0.01); 753 + const span_ = Math.max(vMax - vMin, 0.05); 754 + const vHi_ = vMax + 0.2 * span_; 755 + const vLo_ = vMin - 0.15 * span_; 756 + const yOfV_ = (v) => padT + (vHi_ - v) / (vHi_ - vLo_) * plotH_; 757 + 758 + ctx.fillStyle = theme_.muted; ctx.font = "12px ui-sans-serif, system-ui"; 759 + ctx.fillText("Probe \u2014 V at source node vs time", padL + 8, padT + 14); 760 + 761 + const vStep_ = niceStep(vHi_ - vLo_, 4); 762 + ctx.strokeStyle = theme_.grid; ctx.lineWidth = 1; 763 + for (let i = Math.ceil(vLo_ / vStep_); ; i++) { 764 + const v = i * vStep_; 765 + if (v > vHi_ + vStep_ * 0.01) break; 766 + const yv = yOfV_(v); 767 + if (yv < padT - 1 || yv > padT + plotH_ + 1) continue; 768 + line(ctx, padL, yv, padL + plotW_, yv); 769 + } 770 + line(ctx, padL, padT, padL + plotW_, padT); 771 + line(ctx, padL, padT + plotH_, padL + plotW_, padT + plotH_); 772 + line(ctx, padL, padT, padL, padT + plotH_); 773 + line(ctx, padL + plotW_, padT, padL + plotW_, padT + plotH_); 774 + const yZero_ = yOfV_(0); 775 + if (yZero_ >= padT && yZero_ <= padT + plotH_) { 776 + ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1.2; 777 + line(ctx, padL, yZero_, padL + plotW_, yZero_); 778 + } 779 + 780 + const vDec_ = vStep_ >= 1 ? 0 : vStep_ >= 0.1 ? 1 : 2; 781 + ctx.textAlign = "right"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 782 + for (let i = Math.ceil(vLo_ / vStep_); ; i++) { 783 + const v = i * vStep_; 784 + if (v > vHi_ + vStep_ * 0.01) break; 785 + const yv = yOfV_(v); 786 + if (yv < padT - 1 || yv > padT + plotH_ + 1) continue; 787 + ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1; 788 + line(ctx, padL - 4, yv, padL, yv); 789 + ctx.fillStyle = theme_.muted; 790 + ctx.fillText(v.toFixed(vDec_), padL - 7, yv + 4); 791 + } 792 + ctx.textAlign = "left"; 793 + 794 + const tStep_ = niceStep(tMax, 6); 795 + const tDec_ = tStep_ >= 1 ? 0 : 1; 796 + ctx.textAlign = "center"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 797 + for (let t = 0; t <= tMax + tStep_ * 0.01; t += tStep_) { 798 + const xt = xOfT(t); 799 + if (xt < padL - 1 || xt > padL + plotW_ + 1) continue; 800 + ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1; 801 + line(ctx, xt, padT + plotH_, xt, padT + plotH_ + 4); 802 + ctx.fillStyle = theme_.muted; 803 + ctx.fillText(t.toFixed(tDec_), xt, padT + plotH_ + 16); 804 + } 805 + ctx.textAlign = "left"; 806 + ctx.fillStyle = theme_.muted; ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 807 + ctx.fillText("t / \u03C4d", padL + plotW_ + 6, padT + plotH_ + 4); 808 + 809 + const tnClamped_ = Math.min(tn, tMax); 810 + const kEnd = Math.min(Math.round(tnClamped_ / sim.dt), sim.nSteps - 1); 811 + ctx.strokeStyle = theme_.tdr; ctx.lineWidth = 2.2; 812 + ctx.beginPath(); 813 + for (let k = 0; k <= kEnd; k++) { 814 + const t = k * sim.dt; 815 + if (k === 0) ctx.moveTo(xOfT(t), yOfV_(vArr[k])); 816 + else ctx.lineTo(xOfT(t), yOfV_(vArr[k])); 817 + } 818 + ctx.stroke(); 819 + 820 + if (tn > 0 && tn <= tMax) { 821 + ctx.strokeStyle = theme_.warn; ctx.lineWidth = 1; ctx.setLineDash([4, 5]); 822 + line(ctx, xOfT(tn), padT, xOfT(tn), padT + plotH_); 823 + ctx.setLineDash([]); 824 + } 825 + 826 + const kNow = Math.max(0, Math.min(Math.round(tn / sim.dt), sim.nSteps - 1)); 827 + ctx.fillStyle = theme_.tdr; ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 828 + ctx.textAlign = "right"; 829 + ctx.fillText("V(det) = " + vArr[kNow].toFixed(4) + " V", padL + plotW_ - 6, padT + 18); 830 + ctx.textAlign = "left"; 831 + } 832 + 833 + // ---- Probe canvas (bounce-series mode) ---- 834 + // Draws VS(t) — voltage at the source node (z=0) vs time. 504 835 // The trace grows up to the current tNorm; a cursor line shows current time. 505 - function drawTDR(ctx, w, h, tn, bounce, riseTimeTr, riseShape, timeHorizon, theme) { 836 + function drawProbe(ctx, w, h, tn, bounce, riseTimeTr, riseShape, timeHorizon, theme) { 506 837 ctx.clearRect(0, 0, w, h); 507 838 ctx.fillStyle = theme.panel; 508 839 ctx.fillRect(0, 0, w, h); ··· 539 870 // Title 540 871 ctx.fillStyle = theme.muted; 541 872 ctx.font = "12px ui-sans-serif, system-ui"; 542 - ctx.fillText("TDR \u2014 V at detection point vs time", padL + 8, padT + 14); 873 + ctx.fillText("Probe \u2014 V at source node vs time", padL + 8, padT + 14); 543 874 544 875 // Grid lines 545 876 const vStep = niceStep(vHi - vLo, 4); ··· 670 1001 getTheme, 671 1002 drawCircuit, 672 1003 drawPlot, 673 - drawTDR, 1004 + drawPlotSim, 1005 + drawProbe, 1006 + drawProbeSim, 674 1007 ensurePlotCanvasHeight, 675 1008 }; 676 1009 })();
+17
style.css
··· 102 102 .divider { height: 1px; background: #1b2736; margin: 10px 0; } 103 103 .small { font-size: 14px; color: var(--muted); } 104 104 input[type="checkbox"]{ width: 16px; height: 16px; accent-color: var(--accent); } 105 + .junction-list { display: grid; gap: 4px; margin-top: 4px; } 106 + .junction-item { 107 + display: grid; 108 + grid-template-columns: auto 1fr 1fr auto; 109 + gap: 6px; 110 + align-items: center; 111 + font-size: 14px; 112 + color: var(--muted); 113 + background: #0c1420; 114 + border: 1px solid #24364e; 115 + border-radius: 8px; 116 + padding: 6px 8px; 117 + } 118 + .junction-item select, .junction-item input { width: 100%; } 119 + .load-row { grid-template-columns: auto 1fr 1fr auto; } 120 + .tau-d-row { display: grid; grid-template-columns: 1fr auto; gap: 6px; align-items: center; } 121 + .unit-lbl { color: var(--muted); font-size: 14px; white-space: nowrap; } 105 122 .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-variant-numeric: tabular-nums; } 106 123 .seg-z0-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; } 107 124 .seg-z0-list input { width: 72px; flex: 0 0 72px; }