"use strict"; // Application entry point — state, animation loop, event wiring, UI updates. // Depends on: TLUtils (utils.js), TLPhysics (physics.js), TLRender (render.js) (() => { const { clamp, fmt, resizeCanvasToCSS } = TLUtils; const { computeWaveParams, buildBounceSeries, computeDynamicState, simulateTimeDomain, voltageAt, } = TLPhysics; const { getTheme, drawCircuit, drawPlot, drawPlotSim, drawProbe, drawProbeSim, ensurePlotCanvasHeight } = TLRender; // ---- DOM references ---- const el = { probe: document.getElementById("probe"), circuit: document.getElementById("circuit"), plot: document.getElementById("plot"), startBtn: document.getElementById("startBtn"), pauseBtn: document.getElementById("pauseBtn"), resetBtn: document.getElementById("resetBtn"), Vg: document.getElementById("Vg"), Rg: document.getElementById("Rg"), loadType: document.getElementById("loadType"), loadValue: document.getElementById("loadValue"), loadUnit: document.getElementById("loadUnit"), segCount: document.getElementById("segCount"), segZ0List: document.getElementById("segZ0List"), junctionList: document.getElementById("junctionList"), tauD: document.getElementById("tauD"), secPerTau: document.getElementById("secPerTau"), secPerTauRead: document.getElementById("secPerTauRead"), reflectTol: document.getElementById("reflectTol"), tRead: document.getElementById("tRead"), gLRead: document.getElementById("gLRead"), vsRead: document.getElementById("vsRead"), vlRead: document.getElementById("vlRead"), derivedValues: document.getElementById("derivedValues"), waveValues: document.getElementById("waveValues"), riseMode: document.getElementById("riseMode"), riseTau: document.getElementById("riseTau"), }; // ---- model (physics parameters) ---- const model = { Vg: 5, Rg: 20, segments: [{ Z0: 50 }], // backward-compat for bounce series RL: 30, blocks: [], // set by syncModelFromInputs terminal: { type: 'R', value: 30 }, tau_d: 1e-9, secPerTau: 2.5, reflectTol: 1, riseTimeTr: 0, riseShape: "step", }; // ---- segment + junction input management ---- let segZ0Inputs = []; let junctionInputs = []; // [{typeEl, valueEl}] for N-1 internal junctions const UNIT_FOR_TYPE = { R: 'Ω', C: 'pF', L: 'nH', short: '', none: '' }; function buildSegmentInputs(n) { // Preserve existing Z0 values. const prevZ0 = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); const prevJ = junctionInputs.map((j) => ({ type: j.typeEl.value, val: j.valueEl.value })); el.segZ0List.innerHTML = ""; el.junctionList.innerHTML = ""; segZ0Inputs = []; junctionInputs = []; for (let i = 0; i < n; i++) { // Z0 input for segment i const z0val = (prevZ0[i] != null) ? prevZ0[i] : 50; const inp = document.createElement("input"); inp.type = "number"; inp.min = "0.1"; inp.step = "0.1"; inp.value = z0val; inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; inp.addEventListener("input", rerender); inp.addEventListener("change", rerender); el.segZ0List.appendChild(inp); segZ0Inputs.push(inp); // Junction element between segment i and i+1 if (i < n - 1) { const p = prevJ[i] || { type: 'none', val: '100' }; const row = document.createElement("div"); row.className = "junction-item"; const lbl = document.createElement("span"); lbl.textContent = `${i + 1}\u21C4${i + 2}`; const typeEl = document.createElement("select"); for (const t of ['none', 'R', 'C', 'L', 'short']) { const opt = document.createElement("option"); opt.value = t; opt.textContent = t === 'none' ? '— none —' : t; if (t === p.type) opt.selected = true; typeEl.appendChild(opt); } const valueEl = document.createElement("input"); valueEl.type = "number"; valueEl.min = "0"; valueEl.step = "0.1"; valueEl.value = p.val; valueEl.style.display = (p.type === 'none' || p.type === 'short') ? 'none' : ''; const unitEl = document.createElement("span"); unitEl.textContent = UNIT_FOR_TYPE[p.type] || ''; unitEl.style.minWidth = "24px"; typeEl.addEventListener("change", () => { const t = typeEl.value; valueEl.style.display = (t === 'none' || t === 'short') ? 'none' : ''; unitEl.textContent = UNIT_FOR_TYPE[t] || ''; rerender(); }); valueEl.addEventListener("input", rerender); valueEl.addEventListener("change", rerender); row.appendChild(lbl); row.appendChild(typeEl); row.appendChild(valueEl); row.appendChild(unitEl); el.junctionList.appendChild(row); junctionInputs.push({ typeEl, valueEl }); } } } // Initialise with N=1. buildSegmentInputs(1); // ---- load type control ---- el.loadType.addEventListener("change", () => { const t = el.loadType.value; el.loadValue.disabled = (t === 'open' || t === 'short'); el.loadUnit.textContent = UNIT_FOR_TYPE[t] || ''; rerender(); }); el.loadValue.addEventListener("input", rerender); el.loadValue.addEventListener("change", rerender); // ---- animation state ---- let running = false; let hasStarted = false; let tNorm = 0; let lastTS = null; let timeHorizon = 2.2; let mathjaxTypesetDone = false; let theme = getTheme(); // ---- simulation cache ---- // cachedSim is recomputed whenever the model changes (detected by JSON key). let cachedSim = null; let cachedSimKey = ''; // Returns true if any junction or terminal has reactive (C or L) elements, // meaning we must use simulateTimeDomain for correct physics. function needsMoCSim() { if (!model.blocks) return false; for (const blk of model.blocks) { if (blk.type === 'C' || blk.type === 'L') return true; } if (model.terminal.type === 'C' || model.terminal.type === 'L') return true; return false; } // Returns true if the model has ANY shunt element (R, C, L, or short) at a // junction — i.e., anything beyond the default "just T-line segments" topology. function hasJunctionElements() { if (!model.blocks) return false; for (const blk of model.blocks) { if (blk.type !== 'tl') return true; } return false; } function getOrComputeSim() { const key = JSON.stringify(model.blocks) + JSON.stringify(model.terminal) + model.tau_d + model.riseTimeTr + model.riseShape + model.Vg + model.Rg; if (key !== cachedSimKey) { const tEnd = Math.max(16, timeHorizon + 2); cachedSim = simulateTimeDomain(model, { tEnd, oversample: 128 }); cachedSimKey = key; } return cachedSim; } // ---- model sync ---- function syncModelFromInputs() { model.Vg = parseFloat(el.Vg.value); model.Rg = parseFloat(el.Rg.value); model.secPerTau = parseFloat(el.secPerTau.value); model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); model.riseShape = el.riseMode.value === "step" ? "step" : el.riseMode.value === "linear" ? "linear" : "erf"; model.riseTimeTr = model.riseShape !== "step" ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) : 0; model.tau_d = Math.max(1e-12, parseFloat(el.tauD.value) || 1) * 1e-9; const segsZ0 = segZ0Inputs.map((inp) => Math.max(0.1, parseFloat(inp.value) || 50)); const N = segsZ0.length; const tau = 1 / N; // normalized tau per segment (each segment = 1/N of total τ_d) // Build model.blocks: interleave TL segments with junction shunt elements. const blocks = []; for (let i = 0; i < N; i++) { blocks.push({ type: 'tl', Z0: segsZ0[i], tau }); if (i < N - 1 && junctionInputs[i]) { const jt = junctionInputs[i].typeEl.value; const jvRaw = parseFloat(junctionInputs[i].valueEl.value) || 0; if (jt !== 'none') { // Convert from display units to SI. // R: Ω (direct), C: pF → F (* 1e-12), L: nH → H (* 1e-9) const jv = jt === 'C' ? jvRaw * 1e-12 : jt === 'L' ? jvRaw * 1e-9 : jvRaw; blocks.push({ type: jt, value: jv }); } } } model.blocks = blocks; // Build model.terminal from load type/value. const lt = el.loadType.value; const lvRaw = parseFloat(el.loadValue.value) || 0; if (lt === 'open') { model.terminal = { type: 'open' }; model.RL = Infinity; } else if (lt === 'short') { model.terminal = { type: 'short' }; model.RL = 0; } else if (lt === 'R') { const rv = Math.max(0.001, lvRaw); model.terminal = { type: 'R', value: rv }; model.RL = rv; } else if (lt === 'C') { model.terminal = { type: 'C', value: lvRaw * 1e-12 }; model.RL = Infinity; // cap looks open at DC initially } else if (lt === 'L') { model.terminal = { type: 'L', value: lvRaw * 1e-9 }; model.RL = 0; // inductor shorts at DC } // Backward-compat: model.segments used by buildBounceSeries. model.segments = segsZ0.map((Z0) => ({ Z0 })); } // ---- derived-value readout panel (bounce-series mode only) ---- function updateDerivedDisplays(model, waves, bounce) { const N = model.segments.length; const gS = bounce.gSEffective; const lines = [ `\u0393S = ${fmt(gS, 6)}`, `\u0393L = ${fmt(waves.gL, 6)}`, ]; if (N > 1) { for (let i = 0; i < N - 1; i++) { const Zl = model.segments[i].Z0; const Zr = model.segments[i + 1].Z0; const g = (Zr - Zl) / (Zr + Zl); lines.push(`\u0393${i + 1}\u2192${i + 2} = ${fmt(g, 6)} (${Zl}\u03A9\u2192${Zr}\u03A9)`); } } lines.push(`V1 = ${fmt(waves.V1, 6)} V`); if (N === 1) { const V2calc = waves.gL * waves.V1; const V3calc = gS * V2calc; const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; const v3Reason = v3Suppressed ? `suppressed since |V3| \u2264 \u03B5 (\u03B5=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V1|)` : "nonzero, so re-reflection should appear"; lines.push( `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V1| = ${fmt(bounce.ampTol, 6)} V`, `V2 = \u0393L\u00B7V1 = (${fmt(waves.gL, 6)})\u00B7(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, `V3 = \u0393S\u00B7V2 = (${fmt(gS, 6)})\u00B7(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, `V3 status: ${v3Reason}`, ); } else { lines.push(`Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V1| = ${fmt(bounce.ampTol, 6)} V`); } lines.push(`Total generated waves: ${bounce.series.length}`); el.derivedValues.innerHTML = lines.map((x) => `