this repo has no description
1"use strict";
2
3// Canvas rendering — circuit diagram and V(z) plot.
4// All draw functions are pure with respect to app state; they receive
5// everything they need as parameters (ctx, dimensions, dynamic state, theme).
6//
7// Multi-segment support:
8// drawCircuit takes a `segments` array and draws N abutting T-line boxes.
9// Wave voltage helpers respect each packet's z-range [zStart, zEnd].
10// drawPlot draws light vertical tick marks at internal segment boundaries.
11
12const TLRender = (() => {
13 // ---- plot layout constants ----
14 const PLOT_PANEL_H = 150;
15 const PLOT_PANEL_GAP = 10;
16 const PLOT_PAD_T = 14;
17 const PLOT_PAD_B = 26;
18
19 // ---- theme ----
20 function getTheme() {
21 const get = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
22 return {
23 bg: get("--bg"),
24 panel: get("--panel"),
25 ink: get("--ink"),
26 muted: get("--muted"),
27 grid: get("--grid"),
28 accent: get("--accent"),
29 accent2: get("--accent2"),
30 warn: get("--warn"),
31 ok: get("--ok"),
32 tdr: get("--tdr"),
33 probe2: get("--probe2"),
34 };
35 }
36
37 // ---- primitive helpers ----
38 function line(ctx, x0, y0, x1, y1) {
39 ctx.beginPath();
40 ctx.moveTo(x0, y0);
41 ctx.lineTo(x1, y1);
42 ctx.stroke();
43 }
44
45 function circle(ctx, x, y, r, strokeStyle) {
46 ctx.strokeStyle = strokeStyle;
47 ctx.lineWidth = 2;
48 ctx.beginPath();
49 ctx.arc(x, y, r, 0, Math.PI * 2);
50 ctx.stroke();
51 }
52
53 function circleFill(ctx, x, y, r, fillStyle) {
54 ctx.fillStyle = fillStyle;
55 ctx.beginPath();
56 ctx.arc(x, y, r, 0, Math.PI * 2);
57 ctx.fill();
58 }
59
60 function label(ctx, text, x, y, color) {
61 ctx.fillStyle = color;
62 ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial";
63 ctx.fillText(text, x, y);
64 }
65
66 // Return a "nice" tick step for a given range and target number of ticks.
67 function niceStep(range, targetCount) {
68 const raw = range / targetCount;
69 const mag = Math.pow(10, Math.floor(Math.log10(raw)));
70 const norm = raw / mag;
71 const nice = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10;
72 return nice * mag;
73 }
74
75 // ---- circuit component primitives ----
76 function drawResistor(ctx, x0, y0, x1, y1, zigZagCount, amp, theme) {
77 ctx.strokeStyle = theme.ink;
78 ctx.lineWidth = 2;
79 const dx = x1 - x0, dy = y1 - y0;
80 const len = Math.hypot(dx, dy);
81 if (len < 1e-6) return;
82 const ux = dx / len, uy = dy / len;
83 const px = -uy, py = ux;
84
85 const lead = 10;
86 const start = { x: x0 + ux * lead, y: y0 + uy * lead };
87 const end = { x: x1 - ux * lead, y: y1 - uy * lead };
88
89 line(ctx, x0, y0, start.x, start.y);
90 line(ctx, end.x, end.y, x1, y1);
91
92 const segs = zigZagCount * 2 - 1;
93 const segLen = (len - 2 * lead) / segs;
94 ctx.beginPath();
95 ctx.moveTo(start.x, start.y);
96 for (let i = 1; i < segs; i++) {
97 const s = i * segLen;
98 const flip = (i % 2 === 0) ? -1 : 1;
99 ctx.lineTo(start.x + ux * s + px * amp * flip,
100 start.y + uy * s + py * amp * flip);
101 }
102 ctx.lineTo(end.x, end.y);
103 ctx.stroke();
104 }
105
106 function drawSwitch(ctx, x, y, tn, theme) {
107 const closed = tn > 0;
108 ctx.strokeStyle = theme.ink;
109 ctx.lineWidth = 2;
110 const a = { x: x - 18, y };
111 const b = { x: x + 18, y };
112 circleFill(ctx, a.x, a.y, 3.2, theme.ink);
113 circleFill(ctx, b.x, b.y, 3.2, theme.ink);
114 if (closed) {
115 line(ctx, a.x, a.y, b.x, b.y);
116 } else {
117 line(ctx, a.x + 1, a.y - 1, b.x - 2, b.y - 14);
118 }
119 label(ctx, "t = 0", x - 18, y + 26, theme.muted);
120 }
121
122 function drawVoltageProbe(ctx, x, yTop, yBot, name, theme) {
123 ctx.fillStyle = theme.muted;
124 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
125 ctx.fillText("+", x + 6, yTop + 12);
126 ctx.fillText("\u2212", x + 6, yBot - 4);
127 label(ctx, name, x + 5, (yTop + yBot) / 2 - 6, theme.muted);
128 }
129
130 // ---- shunt element symbols ----
131 // All draw vertically from (x, yA) down to (x, yB), connecting signal to GND.
132
133 function drawShuntR(ctx, x, yA, yB, theme) {
134 ctx.strokeStyle = theme.ink;
135 ctx.lineWidth = 2;
136 drawResistor(ctx, x, yA, x, yB, 4, 7, theme);
137 }
138
139 function drawShuntC(ctx, x, yA, yB, theme) {
140 const midY = (yA + yB) / 2;
141 const plateHalfW = 13;
142 const plateGap = 9;
143 ctx.strokeStyle = theme.ink;
144 ctx.lineWidth = 2;
145 line(ctx, x, yA, x, midY - plateGap / 2);
146 line(ctx, x - plateHalfW, midY - plateGap / 2, x + plateHalfW, midY - plateGap / 2);
147 line(ctx, x - plateHalfW, midY + plateGap / 2, x + plateHalfW, midY + plateGap / 2);
148 line(ctx, x, midY + plateGap / 2, x, yB);
149 }
150
151 function drawShuntL(ctx, x, yA, yB, theme) {
152 const nBumps = 4;
153 const bumpR = 7;
154 const bumpSpan = nBumps * bumpR * 2;
155 const midY = (yA + yB) / 2;
156 const bumpTop = midY - bumpSpan / 2;
157 ctx.strokeStyle = theme.ink;
158 ctx.lineWidth = 2;
159 line(ctx, x, yA, x, bumpTop);
160 for (let i = 0; i < nBumps; i++) {
161 const cy = bumpTop + (i + 0.5) * bumpR * 2;
162 ctx.beginPath();
163 ctx.arc(x, cy, bumpR, Math.PI, 0);
164 ctx.stroke();
165 }
166 line(ctx, x, bumpTop + bumpSpan, x, yB);
167 }
168
169 function drawShuntShort(ctx, x, yA, yB, theme) {
170 ctx.strokeStyle = theme.ink;
171 ctx.lineWidth = 3;
172 line(ctx, x, yA, x, yB);
173 ctx.lineWidth = 2;
174 }
175
176 // Draw a shunt element of given type between (x, yA) and (x, yB), with dots at both nodes.
177 function drawShuntElement(ctx, x, yA, yB, type, theme) {
178 circleFill(ctx, x, yA, 4, theme.ink);
179 circleFill(ctx, x, yB, 4, theme.ink);
180 if (type === 'R') drawShuntR(ctx, x, yA, yB, theme);
181 else if (type === 'C') drawShuntC(ctx, x, yA, yB, theme);
182 else if (type === 'L') drawShuntL(ctx, x, yA, yB, theme);
183 else if (type === 'short') drawShuntShort(ctx, x, yA, yB, theme);
184 }
185
186 // ---- circuit canvas ----
187 // segments = [{Z0}, …] (N entries)
188 // terminal = {type: 'R'|'C'|'L'|'open'|'short', value?}
189 // junctions = [{type: 'none'|'R'|'C'|'L'|'short'}] length N-1 (internal junctions)
190 // wavefronts = [{z, dir}] (optional; positions in [0,1] from active waves)
191 function drawCircuit(ctx, w, h, tn, theme, segments, terminal, junctions, wavefronts) {
192 terminal = terminal || { type: 'open' };
193 junctions = junctions || [];
194 wavefronts = wavefronts || [];
195 const rlIsOpen = terminal.type === 'open';
196 const rlIsShort = terminal.type === 'short';
197 ctx.clearRect(0, 0, w, h);
198 ctx.fillStyle = theme.panel;
199 ctx.fillRect(0, 0, w, h);
200
201 const pad = 18;
202 const yTop = 70;
203 const yBot = 190;
204
205 const xSourceL = pad + 60;
206 const xSwitch = pad + 170;
207 const xTL0 = pad + 240;
208 const xTL1 = w - pad - 210;
209 const xLoad = w - pad - 160;
210 const xRight = w - pad - 90;
211
212 ctx.lineWidth = 2;
213 ctx.strokeStyle = theme.ink;
214
215 // Top and bottom wires — always run to xRight regardless of termination
216 line(ctx, xSwitch + 18, yTop, xTL0, yTop);
217 line(ctx, xTL1, yTop, xRight, yTop);
218 line(ctx, xSourceL, yBot, xTL0, yBot);
219 line(ctx, xTL1, yBot, xRight, yBot);
220
221 // Voltage source
222 const vsx = xSourceL, vsy = (yTop + yBot) / 2;
223 line(ctx, xSourceL, yTop, xSourceL, vsy - 20);
224 line(ctx, xSourceL, vsy + 20, xSourceL, yBot);
225 circle(ctx, vsx, vsy, 20, theme.ink);
226 ctx.fillStyle = theme.ink;
227 ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
228 ctx.fillText("+", vsx - 4, vsy - 6);
229 ctx.fillText("\u2212", vsx - 4, vsy + 14);
230 label(ctx, "Vg", vsx - 38, vsy - 20, theme.muted);
231
232 // Source resistor Rg
233 const r0 = xSourceL + 20, r1 = xSwitch - 30;
234 line(ctx, xSourceL, yTop, r0, yTop);
235 drawResistor(ctx, r0, yTop, r1, yTop, 5, 8, theme);
236 line(ctx, r1, yTop, xSwitch - 18, yTop);
237 label(ctx, "Rg", (r0 + r1) / 2 - 10, yTop - 22, theme.muted);
238
239 drawSwitch(ctx, xSwitch, yTop, tn, theme);
240
241 // Transmission line boxes — N abutting pairs (top + bottom conductor)
242 const N = segments.length;
243 const tlW = xTL1 - xTL0;
244 const segPxW = tlW / N;
245 ctx.lineWidth = 2;
246 ctx.strokeStyle = theme.ink;
247
248 ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial";
249 for (let i = 0; i < N; i++) {
250 const bx = xTL0 + i * segPxW;
251 ctx.strokeRect(bx, yTop - 16, segPxW, 32);
252 ctx.strokeRect(bx, yBot - 16, segPxW, 32);
253
254 // Label above top box: "Z₀" for single segment, "50Ω" etc. for multi
255 const lbl = N === 1
256 ? "Z\u2080"
257 : `${segments[i].Z0}\u03A9`;
258 const lblX = bx + segPxW / 2 - ctx.measureText(lbl).width / 2;
259 label(ctx, lbl, lblX, yTop - 22, theme.muted);
260 }
261
262 // Shunt junction elements (between segments)
263 const shuntYA = yTop + 16; // just below top box
264 const shuntYB = yBot - 16; // just above bottom box
265 for (let i = 0; i < N - 1; i++) {
266 const junc = junctions[i];
267 if (!junc || junc.type === 'none') continue;
268 const xj = xTL0 + (i + 1) * segPxW;
269 drawShuntElement(ctx, xj, shuntYA, shuntYB, junc.type, theme);
270 }
271
272 // Load — resistor, capacitor, inductor, short, or open
273 const rlTop = yTop + 22, rlBot = yBot - 22;
274 ctx.strokeStyle = theme.ink;
275 ctx.lineWidth = 2;
276 if (rlIsOpen) {
277 circleFill(ctx, xRight, yTop, 4.5, theme.ink);
278 circleFill(ctx, xRight, yBot, 4.5, theme.ink);
279 } else if (rlIsShort) {
280 ctx.lineWidth = 3;
281 line(ctx, xLoad, yTop, xLoad, yBot);
282 ctx.lineWidth = 2;
283 } else if (terminal.type === 'R') {
284 line(ctx, xLoad, yTop, xLoad, rlTop);
285 drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 5, 8, theme);
286 line(ctx, xLoad, rlBot, xLoad, yBot);
287 label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted);
288 } else if (terminal.type === 'C') {
289 line(ctx, xLoad, yTop, xLoad, rlTop);
290 drawShuntC(ctx, xLoad, rlTop, rlBot, theme);
291 line(ctx, xLoad, rlBot, xLoad, yBot);
292 label(ctx, "CL", xLoad + 10, (yTop + yBot) / 2, theme.muted);
293 } else if (terminal.type === 'L') {
294 line(ctx, xLoad, yTop, xLoad, rlTop);
295 drawShuntL(ctx, xLoad, rlTop, rlBot, theme);
296 line(ctx, xLoad, rlBot, xLoad, yBot);
297 label(ctx, "LL", xLoad + 10, (yTop + yBot) / 2, theme.muted);
298 }
299
300 // Voltage probes
301 drawVoltageProbe(ctx, xSwitch + 12, yTop, yBot, "VS", theme);
302 drawVoltageProbe(ctx, xLoad + 55, yTop, yBot, "VL", theme);
303
304 // Terminal dots at the VL port — top and bottom
305 if (!rlIsOpen) {
306 circleFill(ctx, xRight, yTop, 3.5, theme.ink);
307 circleFill(ctx, xRight, yBot, 3.5, theme.ink);
308 }
309
310 // Probe 1 — dots on top and bottom wire between switch and T-line input
311 const xDetect = Math.round((xSwitch + 18 + xTL0) / 2);
312 circleFill(ctx, xDetect, yTop, 5.5, theme.tdr);
313 circleFill(ctx, xDetect, yBot, 5.5, theme.tdr);
314
315 // Probe 2 — dots at the load node (far end of last T-line)
316 circleFill(ctx, xLoad, yTop, 5.5, theme.probe2);
317 circleFill(ctx, xLoad, yBot, 5.5, theme.probe2);
318 ctx.fillStyle = theme.tdr;
319 ctx.font = "11px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial";
320 ctx.textAlign = "left";
321
322 // Wavefront markers (dashed vertical lines inside T-line boxes)
323 const wfY0 = yTop - 16;
324 const wfY1 = yBot + 16;
325 for (const wf of wavefronts) {
326 const xw = xTL0 + wf.z * tlW;
327 ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2;
328 ctx.lineWidth = 3;
329 ctx.setLineDash([9, 7]);
330 line(ctx, xw, wfY0, xw, wfY1);
331 ctx.setLineDash([]);
332 }
333
334 label(ctx, "z = 0", xTL0 - 20, yBot + 34, theme.muted);
335 label(ctx, "z = \u2113", xTL1 - 18, yBot + 34, theme.muted);
336 }
337
338 // ---- wave-shape helpers ----
339
340 // Voltage profile of a single wave packet as piecewise-constant segments over [0,1].
341 // The packet only contributes amplitude in its own z-range [zStart, zEnd].
342 function segmentsForWave(wf) {
343 const { dir, zStart, A, front } = wf;
344 let segs;
345 if (dir > 0) {
346 // Nonzero in [zStart, front]; zero elsewhere.
347 segs = [
348 { a: 0, b: zStart, V: 0 },
349 { a: zStart, b: front, V: A },
350 { a: front, b: 1, V: 0 },
351 ];
352 } else {
353 // Leftward: zStart is the right edge; nonzero in [front, zStart].
354 segs = [
355 { a: 0, b: front, V: 0 },
356 { a: front, b: zStart, V: A },
357 { a: zStart, b: 1, V: 0 },
358 ];
359 }
360 const valid = segs.filter(s => s.b > s.a + 1e-9);
361 return valid.length ? valid : [{ a: 0, b: 1, V: 0 }];
362 }
363
364 // Sum of all launched waves as piecewise-constant segments over [0,1].
365 // Breakpoints include every wave's front, zStart, and zEnd so that the
366 // midpoint probe correctly captures all z-range transitions.
367 function totalSegmentsForWaves(waves) {
368 const breakpoints = [0, 1];
369 for (const wf of waves) {
370 breakpoints.push(wf.front);
371 breakpoints.push(wf.zStart);
372 breakpoints.push(wf.zEnd);
373 }
374 breakpoints.sort((a, b) => a - b);
375
376 const pts = [];
377 for (const x of breakpoints) {
378 if (!pts.length || Math.abs(x - pts[pts.length - 1]) > 1e-6) pts.push(x);
379 }
380
381 const segs = [];
382 for (let i = 0; i < pts.length - 1; i++) {
383 const a = pts[i], b = pts[i + 1];
384 const m = (a + b) / 2;
385 let V = 0;
386 for (const wf of waves) {
387 if (wf.dir > 0 && m >= wf.zStart - 1e-9 && m <= wf.front + 1e-9) V += wf.A;
388 if (wf.dir < 0 && m >= wf.front - 1e-9 && m <= wf.zStart + 1e-9) V += wf.A;
389 }
390 segs.push({ a, b, V });
391 }
392 return segs.length ? segs : [{ a: 0, b: 1, V: 0 }];
393 }
394
395 // Draw a piecewise-constant voltage profile.
396 function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) {
397 ctx.strokeStyle = color;
398 ctx.lineWidth = width;
399 ctx.beginPath();
400 ctx.moveTo(xOfZ(segments[0].a), yOfV(segments[0].V));
401 for (const seg of segments) {
402 ctx.lineTo(xOfZ(seg.a), yOfV(seg.V));
403 ctx.lineTo(xOfZ(seg.b), yOfV(seg.V));
404 }
405 ctx.stroke();
406 }
407
408 // Draw a continuous voltage profile by sampling vFn(z) at N evenly-spaced z values.
409 function drawSampledWave(ctx, xOfZ, yOfV, vFn, color, width, N = 400) {
410 ctx.strokeStyle = color;
411 ctx.lineWidth = width;
412 ctx.beginPath();
413 for (let i = 0; i <= N; i++) {
414 const z = i / N;
415 if (i === 0) ctx.moveTo(xOfZ(z), yOfV(vFn(z)));
416 else ctx.lineTo(xOfZ(z), yOfV(vFn(z)));
417 }
418 ctx.stroke();
419 }
420
421 // ---- plot canvas ----
422 // segments = model.segments (used to draw boundary tick marks)
423 function drawPlot(ctx, w, h, tn, dyn, theme, riseTimeTr = 0, riseShape = "step", segments = [{ Z0: 50 }]) {
424 ctx.clearRect(0, 0, w, h);
425 ctx.fillStyle = theme.panel;
426 ctx.fillRect(0, 0, w, h);
427
428 const circuitPad = 18;
429 const xPlot0 = circuitPad + 240;
430 const xPlot1 = w - circuitPad - 210;
431 const plotW = xPlot1 - xPlot0;
432 const xOfZ = (z) => xPlot0 + z * plotW;
433
434 const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n);
435 const sumSegs = totalSegmentsForWaves(launched);
436
437 const maxAbsWave = Math.max(1e-6, ...launched.map((wf) => Math.abs(wf.A)));
438 const maxAbsSum = Math.max(1e-6, sumSegs.reduce((m, s) => Math.max(m, Math.abs(s.V)), 0));
439 const vScale = Math.max(maxAbsWave, maxAbsSum);
440 const vLo = -1.15 * vScale;
441 const vHi = 1.15 * vScale;
442
443 ctx.fillStyle = theme.muted;
444 ctx.font = "12px ui-sans-serif, system-ui";
445 ctx.fillText("Voltage along the T-line", 12, 14);
446
447 // Draw axes/border for one panel, with y-axis tick marks and labels.
448 function drawPanelFrame(top, bot, yOfV, labelText, vLo, vHi) {
449 const step = niceStep(vHi - vLo, 5);
450 const decimals = step >= 1 ? 0 : step >= 0.1 ? 1 : 2;
451 // Index of first tick at or above vLo
452 const iTick0 = Math.ceil(vLo / step);
453
454 // Horizontal grid lines at each tick (drawn first, behind everything)
455 ctx.strokeStyle = theme.grid;
456 ctx.lineWidth = 1;
457 for (let i = iTick0; ; i++) {
458 const v = i * step;
459 if (v > vHi + step * 0.01) break;
460 const yv = yOfV(v);
461 if (yv < top - 1 || yv > bot + 1) continue;
462 line(ctx, xPlot0, yv, xPlot1, yv);
463 }
464
465 // Border (on top of grid lines)
466 ctx.strokeStyle = theme.grid;
467 ctx.lineWidth = 1;
468 line(ctx, xPlot0, top, xPlot1, top);
469 line(ctx, xPlot0, bot, xPlot1, bot);
470 line(ctx, xPlot0, top, xPlot0, bot);
471 line(ctx, xPlot1, top, xPlot1, bot);
472
473 // Zero line — slightly more visible than other grid lines
474 const yZero = yOfV(0);
475 if (yZero >= top - 1 && yZero <= bot + 1) {
476 ctx.strokeStyle = theme.muted;
477 ctx.lineWidth = 1.2;
478 line(ctx, xPlot0, yZero, xPlot1, yZero);
479 }
480
481 // Tick marks and labels on the left y-axis
482 ctx.textAlign = "right";
483 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
484 for (let i = iTick0; ; i++) {
485 const v = i * step;
486 if (v > vHi + step * 0.01) break;
487 const yv = yOfV(v);
488 if (yv < top - 1 || yv > bot + 1) continue;
489 ctx.strokeStyle = theme.muted;
490 ctx.lineWidth = 1;
491 line(ctx, xPlot0 - 4, yv, xPlot0, yv);
492 ctx.fillStyle = theme.muted;
493 ctx.fillText(v.toFixed(decimals), xPlot0 - 7, yv + 4);
494 }
495 ctx.textAlign = "left";
496
497 // Panel title and axis labels
498 ctx.fillStyle = theme.muted;
499 ctx.font = "12px ui-sans-serif, system-ui";
500 ctx.fillText(labelText, xPlot0 + 8, top + 14);
501 ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4);
502
503 // Segment boundary tick marks (for multi-segment lines)
504 const N = segments.length;
505 if (N > 1) {
506 ctx.strokeStyle = theme.grid;
507 ctx.lineWidth = 1;
508 ctx.setLineDash([3, 4]);
509 for (let i = 1; i < N; i++) {
510 const xb = xOfZ(i / N);
511 line(ctx, xb, top, xb, bot);
512 }
513 ctx.setLineDash([]);
514 }
515 }
516
517 // Dashed wave-front position markers
518 function drawFrontMarkers(top, bot, fronts) {
519 if (!fronts.length) return;
520 ctx.strokeStyle = theme.warn;
521 ctx.lineWidth = 1.2;
522 ctx.setLineDash([4, 5]);
523 for (const zf of fronts) line(ctx, xOfZ(zf), top, xOfZ(zf), bot);
524 ctx.setLineDash([]);
525 }
526
527 const panelH = PLOT_PANEL_H;
528 const { waveVoltageAt, totalVoltageAt } = TLPhysics;
529 const smooth = riseTimeTr > 0 && riseShape !== "step";
530
531 // Panel 1 — sum of all waves
532 const top0 = PLOT_PAD_T;
533 const bot0 = top0 + panelH;
534 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8);
535 drawPanelFrame(top0, bot0, y0, "Sum (all waves)", vLo, vHi);
536 if (smooth) {
537 drawSampledWave(ctx, xOfZ, y0, (z) => totalVoltageAt(z, launched, riseTimeTr, riseShape, segments.length), theme.ok, 2.4);
538 } else {
539 drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4);
540 }
541 drawFrontMarkers(top0, bot0, dyn.activeWaves.map((wf) => wf.front));
542
543 // Panel 2 — individual component waves
544 const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP;
545 const bot1 = top1 + panelH;
546 const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8);
547 drawPanelFrame(top1, bot1, y1, "Components (all waves)", vLo, vHi);
548
549 const waveStyles = [
550 { color: theme.accent, dash: [] },
551 { color: theme.accent2, dash: [] },
552 { color: theme.accent, dash: [9, 6] },
553 { color: theme.accent2, dash: [9, 6] },
554 { color: theme.accent, dash: [2, 5] },
555 { color: theme.accent2, dash: [2, 5] },
556 { color: theme.accent, dash: [16, 5] },
557 { color: theme.accent2, dash: [16, 5] },
558 { color: theme.accent, dash: [8, 4, 2, 4] },
559 { color: theme.accent2, dash: [8, 4, 2, 4] },
560 ];
561
562 const FADE_DURATION = 4;
563 const FADE_MIN = 0.08;
564
565 for (let i = 0; i < launched.length; i++) {
566 const wf = launched[i];
567 const style = waveStyles[i % waveStyles.length];
568 // Fade after the wave front has reached its endpoint (tDie).
569 const alpha = TLUtils.clamp(1 - (tn - wf.tDie) / FADE_DURATION, FADE_MIN, 1);
570 ctx.globalAlpha = alpha;
571 ctx.setLineDash(style.dash);
572 if (smooth) {
573 drawSampledWave(ctx, xOfZ, y1, (z) => waveVoltageAt(wf, z, riseTimeTr, riseShape), style.color, 2.0);
574 } else {
575 drawPiecewise(ctx, xOfZ, y1, segmentsForWave(wf), style.color, 2.0);
576 }
577 ctx.setLineDash([]);
578 ctx.globalAlpha = 1;
579 }
580 drawFrontMarkers(top1, bot1, launched.filter((wf) => wf.u < Math.abs(wf.zEnd - wf.zStart)).map((wf) => wf.front));
581
582 // z-axis endpoint labels
583 ctx.fillStyle = theme.muted;
584 ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
585 ctx.fillText("0", xPlot0 - 4, h - 6);
586 ctx.fillText("\u2113", xPlot1 - 4, h - 6);
587 }
588
589 // ---- plot canvas (MoC sim mode) ----
590 // Draws total V(z,t) and per-segment V⁺/V⁻ from simulateTimeDomain output.
591 // sim: return value of simulateTimeDomain
592 // tIdx: time step index (tNorm / sim.dt, clamped to [0, nSteps-1])
593 function drawPlotSim(ctx, w, h, tIdx, sim, theme) {
594 ctx.clearRect(0, 0, w, h);
595 ctx.fillStyle = theme.panel;
596 ctx.fillRect(0, 0, w, h);
597
598 const circuitPad = 18;
599 const xPlot0 = circuitPad + 240;
600 const xPlot1 = w - circuitPad - 210;
601 const plotW = xPlot1 - xPlot0;
602 const xOfZ = (z) => xPlot0 + z * plotW;
603
604 const { voltageAt } = TLPhysics;
605 const N = sim.segs.length;
606 const step = Math.max(0, Math.min(tIdx, sim.nSteps - 1));
607
608 // Sample total voltage at evenly-spaced z values
609 const NPTS = 400;
610 const vArr = [];
611 for (let i = 0; i <= NPTS; i++) {
612 vArr.push(voltageAt(i / NPTS, step, sim));
613 }
614
615 // Per-segment V⁺ and V⁻ (sampled from vPlusHist / vMinusHist via voltageAt internals)
616 // We read them directly from the history arrays using the same delay logic as voltageAt.
617 const totalTau = sim.segs.reduce((s, seg) => s + seg.tau, 0);
618
619 const vScale = Math.max(1e-6, ...vArr.map(Math.abs));
620 const vLo = -1.15 * vScale, vHi = 1.15 * vScale;
621
622 ctx.fillStyle = theme.muted;
623 ctx.font = "12px ui-sans-serif, system-ui";
624 ctx.fillText("Voltage along the T-line (MoC simulation)", 12, 14);
625
626 const panelH = PLOT_PANEL_H;
627 const top0 = PLOT_PAD_T;
628 const bot0 = top0 + panelH;
629 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8);
630
631 // Panel frame helper (inline)
632 function drawFrame(top, bot, yOfV, labelText) {
633 const step_ = niceStep(vHi - vLo, 5);
634 const dec = step_ >= 1 ? 0 : step_ >= 0.1 ? 1 : 2;
635 const iTick0 = Math.ceil(vLo / step_);
636 ctx.strokeStyle = theme.grid; ctx.lineWidth = 1;
637 for (let i = iTick0; ; i++) {
638 const v = i * step_;
639 if (v > vHi + step_ * 0.01) break;
640 const yv = yOfV(v);
641 if (yv < top - 1 || yv > bot + 1) continue;
642 line(ctx, xPlot0, yv, xPlot1, yv);
643 }
644 ctx.strokeStyle = theme.grid; ctx.lineWidth = 1;
645 line(ctx, xPlot0, top, xPlot1, top);
646 line(ctx, xPlot0, bot, xPlot1, bot);
647 line(ctx, xPlot0, top, xPlot0, bot);
648 line(ctx, xPlot1, top, xPlot1, bot);
649 const yZero = yOfV(0);
650 if (yZero >= top - 1 && yZero <= bot + 1) {
651 ctx.strokeStyle = theme.muted; ctx.lineWidth = 1.2;
652 line(ctx, xPlot0, yZero, xPlot1, yZero);
653 }
654 ctx.textAlign = "right"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
655 for (let i = iTick0; ; i++) {
656 const v = i * step_;
657 if (v > vHi + step_ * 0.01) break;
658 const yv = yOfV(v);
659 if (yv < top - 1 || yv > bot + 1) continue;
660 ctx.strokeStyle = theme.muted; ctx.lineWidth = 1;
661 line(ctx, xPlot0 - 4, yv, xPlot0, yv);
662 ctx.fillStyle = theme.muted;
663 ctx.fillText(v.toFixed(dec), xPlot0 - 7, yv + 4);
664 }
665 ctx.textAlign = "left";
666 ctx.fillStyle = theme.muted; ctx.font = "12px ui-sans-serif, system-ui";
667 ctx.fillText(labelText, xPlot0 + 8, top + 14);
668 ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4);
669 // Segment boundary tick marks
670 if (N > 1) {
671 let cumZ = 0;
672 ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; ctx.setLineDash([3, 4]);
673 for (let i = 0; i < N - 1; i++) {
674 cumZ += sim.segs[i].tau / totalTau;
675 const xb = xOfZ(cumZ);
676 line(ctx, xb, top, xb, bot);
677 }
678 ctx.setLineDash([]);
679 }
680 }
681
682 drawFrame(top0, bot0, y0, "V(z, t) — total");
683
684 // Draw total V(z,t)
685 ctx.strokeStyle = theme.ok; ctx.lineWidth = 2.4;
686 ctx.beginPath();
687 for (let i = 0; i <= NPTS; i++) {
688 const x = xOfZ(i / NPTS), y = y0(vArr[i]);
689 if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
690 }
691 ctx.stroke();
692
693 // Panel 2: V⁺ and V⁻ per segment
694 const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP;
695 const bot1 = top1 + panelH;
696 const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8);
697 drawFrame(top1, bot1, y1, "V\u207A (forward) and V\u207B (backward) per segment");
698
699 let cumZ = 0;
700 for (let i = 0; i < N; i++) {
701 const segFrac = sim.segs[i].tau / totalTau;
702 const D = sim.nDelay[i];
703 const zL = cumZ, zR = cumZ + segFrac;
704
705 // Sample V⁺ and V⁻ across this segment
706 const NSEG = Math.max(2, Math.round(NPTS * segFrac));
707 ctx.strokeStyle = theme.accent; ctx.lineWidth = 1.8;
708 ctx.beginPath();
709 for (let j = 0; j <= NSEG; j++) {
710 const f = j / NSEG;
711 const kFwd = Math.round(f * D);
712 const tF = step - kFwd;
713 const vP = (tF >= 0) ? sim.vPlusHist[i][tF] : 0;
714 const x = xOfZ(zL + f * segFrac), y = y1(vP);
715 if (j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
716 }
717 ctx.stroke();
718
719 ctx.strokeStyle = theme.accent2; ctx.lineWidth = 1.8;
720 ctx.beginPath();
721 for (let j = 0; j <= NSEG; j++) {
722 const f = j / NSEG;
723 const kBwd = Math.round((1 - f) * D);
724 const tB = step - kBwd;
725 const vM = (tB >= 0) ? sim.vMinusHist[i][tB] : 0;
726 const x = xOfZ(zL + f * segFrac), y = y1(vM);
727 if (j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
728 }
729 ctx.stroke();
730
731 cumZ += segFrac;
732 }
733
734 // z-axis labels
735 ctx.fillStyle = theme.muted; ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
736 ctx.fillText("0", xPlot0 - 4, h - 6);
737 ctx.fillText("\u2113", xPlot1 - 4, h - 6);
738 }
739
740 // ---- Probe canvas (MoC sim mode) ----
741 // Draws V_source(t) from sim.nodeV[0].
742 function drawProbeSim(ctx, w, h, tn, sim, timeHorizon_, theme_) {
743 ctx.clearRect(0, 0, w, h);
744 ctx.fillStyle = theme_.panel;
745 ctx.fillRect(0, 0, w, h);
746
747 const padL = 55, padR = 30, padT = 20, padB = 30;
748 const plotW_ = w - padL - padR;
749 const plotH_ = h - padT - padB;
750 const tMax = timeHorizon_;
751 const xOfT = (t) => padL + (t / tMax) * plotW_;
752
753 const vArr = sim.nodeV[0];
754 const vArrL = sim.nodeV[sim.nodeV.length - 1];
755 let vMax = 0, vMin = 0;
756 for (let k = 0; k < sim.nSteps; k++) {
757 if (vArr[k] > vMax) vMax = vArr[k];
758 if (vArr[k] < vMin) vMin = vArr[k];
759 if (vArrL[k] > vMax) vMax = vArrL[k];
760 if (vArrL[k] < vMin) vMin = vArrL[k];
761 }
762 vMax = Math.max(vMax, 0.01);
763 const span_ = Math.max(vMax - vMin, 0.05);
764 const vHi_ = vMax + 0.2 * span_;
765 const vLo_ = vMin - 0.15 * span_;
766 const yOfV_ = (v) => padT + (vHi_ - v) / (vHi_ - vLo_) * plotH_;
767
768 ctx.fillStyle = theme_.muted; ctx.font = "12px ui-sans-serif, system-ui";
769 ctx.fillText("Probe \u2014 node voltages vs time", padL + 8, padT + 14);
770
771 const vStep_ = niceStep(vHi_ - vLo_, 4);
772 ctx.strokeStyle = theme_.grid; ctx.lineWidth = 1;
773 for (let i = Math.ceil(vLo_ / vStep_); ; i++) {
774 const v = i * vStep_;
775 if (v > vHi_ + vStep_ * 0.01) break;
776 const yv = yOfV_(v);
777 if (yv < padT - 1 || yv > padT + plotH_ + 1) continue;
778 line(ctx, padL, yv, padL + plotW_, yv);
779 }
780 line(ctx, padL, padT, padL + plotW_, padT);
781 line(ctx, padL, padT + plotH_, padL + plotW_, padT + plotH_);
782 line(ctx, padL, padT, padL, padT + plotH_);
783 line(ctx, padL + plotW_, padT, padL + plotW_, padT + plotH_);
784 const yZero_ = yOfV_(0);
785 if (yZero_ >= padT && yZero_ <= padT + plotH_) {
786 ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1.2;
787 line(ctx, padL, yZero_, padL + plotW_, yZero_);
788 }
789
790 const vDec_ = vStep_ >= 1 ? 0 : vStep_ >= 0.1 ? 1 : 2;
791 ctx.textAlign = "right"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
792 for (let i = Math.ceil(vLo_ / vStep_); ; i++) {
793 const v = i * vStep_;
794 if (v > vHi_ + vStep_ * 0.01) break;
795 const yv = yOfV_(v);
796 if (yv < padT - 1 || yv > padT + plotH_ + 1) continue;
797 ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1;
798 line(ctx, padL - 4, yv, padL, yv);
799 ctx.fillStyle = theme_.muted;
800 ctx.fillText(v.toFixed(vDec_), padL - 7, yv + 4);
801 }
802 ctx.textAlign = "left";
803
804 const tStep_ = niceStep(tMax, 6);
805 const tDec_ = tStep_ >= 1 ? 0 : 1;
806 ctx.textAlign = "center"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
807 for (let t = 0; t <= tMax + tStep_ * 0.01; t += tStep_) {
808 const xt = xOfT(t);
809 if (xt < padL - 1 || xt > padL + plotW_ + 1) continue;
810 ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1;
811 line(ctx, xt, padT + plotH_, xt, padT + plotH_ + 4);
812 ctx.fillStyle = theme_.muted;
813 ctx.fillText(t.toFixed(tDec_), xt, padT + plotH_ + 16);
814 }
815 ctx.textAlign = "left";
816 ctx.fillStyle = theme_.muted; ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
817 ctx.fillText("t / \u03C4d", padL + plotW_ + 6, padT + plotH_ + 4);
818
819 const tnClamped_ = Math.min(tn, tMax);
820 const kEnd = Math.min(Math.round(tnClamped_ / sim.dt), sim.nSteps - 1);
821
822 // VS trace (probe 1 — orange)
823 ctx.strokeStyle = theme_.tdr; ctx.lineWidth = 2.2;
824 ctx.beginPath();
825 for (let k = 0; k <= kEnd; k++) {
826 const t = k * sim.dt;
827 if (k === 0) ctx.moveTo(xOfT(t), yOfV_(vArr[k]));
828 else ctx.lineTo(xOfT(t), yOfV_(vArr[k]));
829 }
830 ctx.stroke();
831
832 // VL trace (probe 2 — blue)
833 ctx.strokeStyle = theme_.probe2; ctx.lineWidth = 2.2;
834 ctx.beginPath();
835 for (let k = 0; k <= kEnd; k++) {
836 const t = k * sim.dt;
837 if (k === 0) ctx.moveTo(xOfT(t), yOfV_(vArrL[k]));
838 else ctx.lineTo(xOfT(t), yOfV_(vArrL[k]));
839 }
840 ctx.stroke();
841
842 if (tn > 0 && tn <= tMax) {
843 ctx.strokeStyle = theme_.warn; ctx.lineWidth = 1; ctx.setLineDash([4, 5]);
844 line(ctx, xOfT(tn), padT, xOfT(tn), padT + plotH_);
845 ctx.setLineDash([]);
846 }
847
848 const kNow = Math.max(0, Math.min(Math.round(tn / sim.dt), sim.nSteps - 1));
849 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
850 ctx.textAlign = "right";
851 ctx.fillStyle = theme_.tdr;
852 ctx.fillText("VS = " + vArr[kNow].toFixed(4) + " V", padL + plotW_ - 6, padT + 18);
853 ctx.fillStyle = theme_.probe2;
854 ctx.fillText("VL = " + vArrL[kNow].toFixed(4) + " V", padL + plotW_ - 6, padT + 34);
855 ctx.textAlign = "left";
856 }
857
858 // ---- Probe canvas (bounce-series mode) ----
859 // Draws VS(t) — voltage at the source node (z=0) vs time.
860 // The trace grows up to the current tNorm; a cursor line shows current time.
861 function drawProbe(ctx, w, h, tn, bounce, riseTimeTr, riseShape, timeHorizon, theme) {
862 ctx.clearRect(0, 0, w, h);
863 ctx.fillStyle = theme.panel;
864 ctx.fillRect(0, 0, w, h);
865
866 const padL = 55, padR = 30, padT = 20, padB = 30;
867 const plotW = w - padL - padR;
868 const plotH = h - padT - padB;
869
870 const tMax = timeHorizon;
871 const xOfT = (t) => padL + (t / tMax) * plotW;
872
873 const { sumEventsAtTime, sumEventsWithRise, sumEventsWithLinearRamp } = TLPhysics;
874 const eventsAt = (evts, t) => {
875 if (riseTimeTr <= 0 || riseShape === "step") return sumEventsAtTime(evts, t);
876 if (riseShape === "linear") return sumEventsWithLinearRamp(evts, t, riseTimeTr);
877 return sumEventsWithRise(evts, t, riseTimeTr);
878 };
879 const vsAtTime = (t) => eventsAt(bounce.srcEvents, t);
880 const vlAtTime = (t) => eventsAt(bounce.loadEvents, t);
881
882 // y range from both srcEvents and loadEvents cumulative sums
883 const events = [...bounce.srcEvents].sort((a, b) => a.t - b.t);
884 const eventsL = [...bounce.loadEvents].sort((a, b) => a.t - b.t);
885 let cumV = 0, vMax = 0, vMin = 0;
886 for (const ev of events) {
887 cumV += ev.dV;
888 if (cumV > vMax) vMax = cumV;
889 if (cumV < vMin) vMin = cumV;
890 }
891 cumV = 0;
892 for (const ev of eventsL) {
893 cumV += ev.dV;
894 if (cumV > vMax) vMax = cumV;
895 if (cumV < vMin) vMin = cumV;
896 }
897 vMax = Math.max(vMax, 0.01);
898 vMin = Math.min(vMin, 0);
899 const span = Math.max(vMax - vMin, 0.05);
900 const vHi = vMax + 0.2 * span;
901 const vLo = vMin - 0.15 * span;
902 const yOfV = (v) => padT + (vHi - v) / (vHi - vLo) * plotH;
903
904 // Title
905 ctx.fillStyle = theme.muted;
906 ctx.font = "12px ui-sans-serif, system-ui";
907 ctx.fillText("Probe \u2014 node voltages vs time", padL + 8, padT + 14);
908
909 // Grid lines
910 const vStep = niceStep(vHi - vLo, 4);
911 ctx.strokeStyle = theme.grid;
912 ctx.lineWidth = 1;
913 for (let i = Math.ceil(vLo / vStep); ; i++) {
914 const v = i * vStep;
915 if (v > vHi + vStep * 0.01) break;
916 const yv = yOfV(v);
917 if (yv < padT - 1 || yv > padT + plotH + 1) continue;
918 line(ctx, padL, yv, padL + plotW, yv);
919 }
920
921 // Border
922 ctx.strokeStyle = theme.grid;
923 ctx.lineWidth = 1;
924 line(ctx, padL, padT, padL + plotW, padT);
925 line(ctx, padL, padT + plotH, padL + plotW, padT + plotH);
926 line(ctx, padL, padT, padL, padT + plotH);
927 line(ctx, padL + plotW, padT, padL + plotW, padT + plotH);
928
929 // Zero line
930 const yZero = yOfV(0);
931 if (yZero >= padT && yZero <= padT + plotH) {
932 ctx.strokeStyle = theme.muted;
933 ctx.lineWidth = 1.2;
934 line(ctx, padL, yZero, padL + plotW, yZero);
935 }
936
937 // Y-axis ticks + labels
938 const vDec = vStep >= 1 ? 0 : vStep >= 0.1 ? 1 : 2;
939 ctx.textAlign = "right";
940 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
941 for (let i = Math.ceil(vLo / vStep); ; i++) {
942 const v = i * vStep;
943 if (v > vHi + vStep * 0.01) break;
944 const yv = yOfV(v);
945 if (yv < padT - 1 || yv > padT + plotH + 1) continue;
946 ctx.strokeStyle = theme.muted;
947 ctx.lineWidth = 1;
948 line(ctx, padL - 4, yv, padL, yv);
949 ctx.fillStyle = theme.muted;
950 ctx.fillText(v.toFixed(vDec), padL - 7, yv + 4);
951 }
952 ctx.textAlign = "left";
953
954 // X-axis ticks + labels
955 const tStep = niceStep(tMax, 6);
956 const tDec = tStep >= 1 ? 0 : 1;
957 ctx.textAlign = "center";
958 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
959 for (let t = 0; t <= tMax + tStep * 0.01; t += tStep) {
960 const xt = xOfT(t);
961 if (xt < padL - 1 || xt > padL + plotW + 1) continue;
962 ctx.strokeStyle = theme.muted;
963 ctx.lineWidth = 1;
964 line(ctx, xt, padT + plotH, xt, padT + plotH + 4);
965 ctx.fillStyle = theme.muted;
966 ctx.fillText(t.toFixed(tDec), xt, padT + plotH + 16);
967 }
968 ctx.textAlign = "left";
969
970 // X-axis label
971 ctx.fillStyle = theme.muted;
972 ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
973 ctx.fillText("t / \u03C4d", padL + plotW + 6, padT + plotH + 4);
974
975 const tnClamped = Math.min(tn, tMax);
976 const smooth = riseTimeTr > 0 && riseShape !== "step";
977
978 // Helper: draw a staircase trace from an event list
979 function drawStaircase(evts, color) {
980 ctx.strokeStyle = color; ctx.lineWidth = 2.2;
981 ctx.beginPath();
982 let prevV = 0;
983 ctx.moveTo(xOfT(0), yOfV(0));
984 for (const ev of evts) {
985 if (ev.t > tnClamped) break;
986 ctx.lineTo(xOfT(ev.t), yOfV(prevV));
987 prevV += ev.dV;
988 ctx.lineTo(xOfT(ev.t), yOfV(prevV));
989 }
990 ctx.lineTo(xOfT(tnClamped), yOfV(prevV));
991 ctx.stroke();
992 }
993
994 // VS trace (probe 1 — orange)
995 if (smooth) {
996 ctx.strokeStyle = theme.tdr; ctx.lineWidth = 2.2;
997 ctx.beginPath();
998 for (let i = 0; i <= 500; i++) {
999 const t = (i / 500) * tnClamped;
1000 if (i === 0) ctx.moveTo(xOfT(t), yOfV(vsAtTime(t)));
1001 else ctx.lineTo(xOfT(t), yOfV(vsAtTime(t)));
1002 }
1003 ctx.stroke();
1004 } else {
1005 drawStaircase(events, theme.tdr);
1006 }
1007
1008 // VL trace (probe 2 — blue)
1009 if (smooth) {
1010 ctx.strokeStyle = theme.probe2; ctx.lineWidth = 2.2;
1011 ctx.beginPath();
1012 for (let i = 0; i <= 500; i++) {
1013 const t = (i / 500) * tnClamped;
1014 if (i === 0) ctx.moveTo(xOfT(t), yOfV(vlAtTime(t)));
1015 else ctx.lineTo(xOfT(t), yOfV(vlAtTime(t)));
1016 }
1017 ctx.stroke();
1018 } else {
1019 drawStaircase(eventsL, theme.probe2);
1020 }
1021
1022 // Current-time cursor (vertical dashed line)
1023 if (tn > 0 && tn <= tMax) {
1024 ctx.strokeStyle = theme.warn;
1025 ctx.lineWidth = 1;
1026 ctx.setLineDash([4, 5]);
1027 line(ctx, xOfT(tn), padT, xOfT(tn), padT + plotH);
1028 ctx.setLineDash([]);
1029 }
1030
1031 // Current voltage readouts
1032 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
1033 ctx.textAlign = "right";
1034 ctx.fillStyle = theme.tdr;
1035 ctx.fillText("VS = " + vsAtTime(tnClamped).toFixed(4) + " V", padL + plotW - 6, padT + 18);
1036 ctx.fillStyle = theme.probe2;
1037 ctx.fillText("VL = " + vlAtTime(tnClamped).toFixed(4) + " V", padL + plotW - 6, padT + 34);
1038 ctx.textAlign = "left";
1039 }
1040
1041 // Resize the plot canvas height to fit the given number of panels.
1042 function ensurePlotCanvasHeight(plotEl, nPanels) {
1043 const targetH = Math.round(
1044 PLOT_PAD_T + PLOT_PAD_B +
1045 nPanels * PLOT_PANEL_H +
1046 (nPanels - 1) * PLOT_PANEL_GAP
1047 );
1048 const curr = parseInt(plotEl.getAttribute("height") || "0", 10);
1049 if (curr !== targetH) plotEl.setAttribute("height", String(targetH));
1050 }
1051
1052 return {
1053 PLOT_PANEL_H, PLOT_PANEL_GAP, PLOT_PAD_T, PLOT_PAD_B,
1054 getTheme,
1055 drawCircuit,
1056 drawPlot,
1057 drawPlotSim,
1058 drawProbe,
1059 drawProbeSim,
1060 ensurePlotCanvasHeight,
1061 };
1062})();