feat(labs): wave 2 — depth features across 6 sims

Электрические цепи (circuit):
- Индуктивность L как новый компонент (1–1000 мГн, шорт в DC, jωL в AC)
- RLC preset для демонстрации резонанса
- Осциллограф: U(t)/I(t) для выбранного компонента, 100 sample, dual-axis
- Heatmap мощности: радиальный градиент halo от blue→red пропорционально P=UI

Стереометрия 3D (stereo):
- Сечение через 3 произвольные точки: pick на гранях/рёбрах/вершинах
- Плоскость + полигон пересечения с авто-определением типа (3–6-угольник) и площадью
- Step-by-step режим: визуализация P1→линия→P2→линия→P3→плоскость→сечение
- Поддержка всех solids (включая cylinder/cone через sampling fallback)

Планиметрия (geometry):
- Задачник framework: CHALLENGES[] с setup/check функциями
- 5 стартовых задач: серединный перпендикуляр, биссектриса, описанная окружность, ГМТ, касательная
- Авто-checker: толерантности ±0.5° для углов, ±1–5% для расстояний
- UI: collapsible панель с статус-иконками, конфетти + «Молодец!» на success

Электромагнитные поля (emfield):
- Preset «Тороид»: 16+16 проводов в концентрических кольцах
- Поверхность Гаусса: draggable круг, считает Φ = q_enc/ε₀, подсвечивает охваченные заряды
- Motional EMF: draggable rod, arrow-keys управление, считает ε = ∫(v×B)·dl

Химическая песочница (chemsandbox):
- Live-overlay с уравнением реакции: молекулярное / полное ионное / сокращённое ионное
- Coverage: 49/49 молекулярных, 34/49 ионных, 36/49 сокращённых
- Auto-hide через 5 сек, fade-in animation, цветовая кодировка типов

Волны и звук (waves):
- Doppler: source+observer drag, expanding wavefronts, f_obs формула, Mach cone при v>c
- Beats: f1+f2, sum waveform с envelope, индикация f_beat=|f1-f2|
- Spectrum (DFT): N=256 samples pure JS, bar-chart с пиками и labels, «Добавить гармонику»

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-23 12:48:14 +03:00
parent 7f75c96acd
commit 8f30a8cef6
8 changed files with 2367 additions and 36 deletions
+429 -9
View File
@@ -64,6 +64,26 @@ class EMFieldSim {
_dragging: false,
};
/* Gauss surface (electric flux, E / combined modes) */
this._gauss = {
on: false,
x: 0, y: 0,
r: 70,
_dragging: false,
};
/* motional EMF rod (B / combined modes) */
this._rod = {
on: false,
x1: 0, y1: 0, x2: 0, y2: 0,
vx: 0, vy: 0, // current velocity px/s
_dragging: false,
_dragOffX: 0, _dragOffY: 0,
_keys: {}, // keys held
_raf: null,
_last: 0,
};
/* test particle */
this._particle = null;
this.particleOn = false;
@@ -124,6 +144,9 @@ class EMFieldSim {
this._cond.x1 = this.W * 0.25; this._cond.y1 = this.H * 0.5;
this._cond.x2 = this.W * 0.75; this._cond.y2 = this.H * 0.5;
this._flux.x = this.W * 0.5; this._flux.y = this.H * 0.35;
this._gauss.x = this.W * 0.5; this._gauss.y = this.H * 0.5;
this._rod.x1 = this.W * 0.5; this._rod.y1 = this.H * 0.3;
this._rod.x2 = this.W * 0.5; this._rod.y2 = this.H * 0.7;
}
this._cmBDirty = true;
@@ -185,6 +208,9 @@ class EMFieldSim {
if (this._pRaf) { cancelAnimationFrame(this._pRaf); this._pRaf = null; }
this._cond.on = false;
this._flux.on = false;
this._gauss.on = false;
if (this._rod._raf) { cancelAnimationFrame(this._rod._raf); this._rod._raf = null; }
this._rod.on = false;
this._invalidateAll();
this.draw();
if (this.onUpdate) this.onUpdate(this.info());
@@ -225,6 +251,113 @@ class EMFieldSim {
this.draw();
}
/* Gauss surface (E mode, electric flux) */
toggleGauss() {
this._gauss.on = !this._gauss.on;
this.draw();
}
setGaussR(r) {
this._gauss.r = r;
this.draw();
}
/* Motional EMF rod (B mode) */
toggleRod() {
const rod = this._rod;
rod.on = !rod.on;
if (rod.on) {
rod.vx = 0; rod.vy = 0;
rod._last = performance.now();
this._tickRod();
} else {
if (rod._raf) { cancelAnimationFrame(rod._raf); rod._raf = null; }
this.draw();
}
if (this.onUpdate) this.onUpdate(this.info());
}
_tickRod() {
const rod = this._rod;
if (!rod.on) return;
const now = performance.now();
const dt = Math.min((now - rod._last) * 0.001, 0.05); // seconds
rod._last = now;
/* keyboard-driven acceleration: arrow keys → velocity */
const speed = 90; // px/s max
let ax = 0, ay = 0;
if (rod._keys['ArrowLeft']) ax -= 1;
if (rod._keys['ArrowRight']) ax += 1;
if (rod._keys['ArrowUp']) ay -= 1;
if (rod._keys['ArrowDown']) ay += 1;
if (ax !== 0 || ay !== 0) {
const len = Math.hypot(ax, ay);
rod.vx = (ax / len) * speed;
rod.vy = (ay / len) * speed;
} else {
/* friction */
rod.vx *= 0.88;
rod.vy *= 0.88;
if (Math.hypot(rod.vx, rod.vy) < 0.5) { rod.vx = 0; rod.vy = 0; }
}
/* move rod */
rod.x1 += rod.vx * dt;
rod.y1 += rod.vy * dt;
rod.x2 += rod.vx * dt;
rod.y2 += rod.vy * dt;
/* clamp to canvas */
const margin = 10;
const minX = Math.min(rod.x1, rod.x2), maxX = Math.max(rod.x1, rod.x2);
const minY = Math.min(rod.y1, rod.y2), maxY = Math.max(rod.y1, rod.y2);
if (minX < margin) { const d = margin - minX; rod.x1 += d; rod.x2 += d; }
if (maxX > this.W - margin) { const d = maxX - (this.W - margin); rod.x1 -= d; rod.x2 -= d; }
if (minY < margin) { const d = margin - minY; rod.y1 += d; rod.y2 += d; }
if (maxY > this.H - margin) { const d = maxY - (this.H - margin); rod.y1 -= d; rod.y2 -= d; }
this.draw();
if (this.onUpdate) this.onUpdate(this.info());
rod._raf = requestAnimationFrame(() => this._tickRod());
}
/* Compute motional EMF = integral of (v × B) · dl along rod */
_rodEMF() {
const rod = this._rod;
const Lx = rod.x2 - rod.x1, Ly = rod.y2 - rod.y1;
const L = Math.hypot(Lx, Ly);
if (L < 1) return { emf: 0, avgB: 0, v: 0 };
/* dl unit vector */
const dlx = Lx / L, dly = Ly / L;
const v = Math.hypot(rod.vx, rod.vy);
const N = 20; // integration samples
let sum = 0, avgB = 0;
for (let k = 0; k <= N; k++) {
const t = k / N;
const px = rod.x1 + Lx * t, py = rod.y1 + Ly * t;
const { bx, by, mag } = this._bField(px, py);
avgB += mag;
/* (v × B) in 2D: vx*By - vy*Bx gives z-component of (v×B)
(v×B)·dl = (vx·By - vy·Bx)·dlx - ... → in 2D project back:
(v×B) is a vector: if v=(vx,vy,0), B=(Bx,By,0) →
v×B = (vy·0-0·By, 0·Bx-vx·0, vx·By-vy·Bx) = (0,0,vx·By-vy·Bx)
But B here is in-plane; we treat |B| as out-of-plane Bz for the 2D sim.
So B = (0,0,Bz) where Bz = mag (or -mag depending on orientation sign).
We use bx,by as in-plane → but physically they represent the field in the plane.
For motional EMF in 2D: use Bz=mag (perpendicular to plane convention).
(v×Bz_hat)·dl = (vy·Bz)·dlx + (-vx·Bz)·dly */
const Beff = mag * 0.00012; // same scale used in particle simulation
const vCrossB_x = rod.vy * Beff;
const vCrossB_y = -rod.vx * Beff;
sum += (vCrossB_x * dlx + vCrossB_y * dly);
}
avgB /= (N + 1);
const emf = sum * L / (N + 1); // Riemann sum → integral
return { emf, avgB, v };
}
/* ──────────────────────────────
Particle
────────────────────────────── */
@@ -367,6 +500,18 @@ class EMFieldSim {
}
break;
}
case 'toroid': {
/* toroid cross-section: inner ring (wire-out) + outer ring (wire-in)
This approximates a toroid where B is confined inside the winding.
16 wire-out at radius r1, 16 wire-in at radius r2 (concentric). */
const n = 16, r1 = 75, r2 = 130;
for (let i = 0; i < n; i++) {
const a = (i / n) * Math.PI * 2;
this._pushWire(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1, 'out');
this._pushWire(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2, 'in');
}
break;
}
}
this._invalidateAll();
this.draw();
@@ -466,18 +611,50 @@ class EMFieldSim {
const out = wires.filter(w => w.I > 0).length;
const inn = wires.filter(w => w.I < 0).length;
const condOn = this._cond.on;
const fluxOn = this._flux.on;
const ampere = condOn ? this._ampereForce() : null;
const Fz = ampere ? ampere.Fz : 0;
const flux = fluxOn ? this._fluxValue() : 0;
const condOn = this._cond.on;
const fluxOn = this._flux.on;
const gaussOn = this._gauss.on;
const rodOn = this._rod.on;
const ampere = condOn ? this._ampereForce() : null;
const Fz = ampere ? ampere.Fz : 0;
const flux = fluxOn ? this._fluxValue() : 0;
/* Gauss surface: exact (sum q_enc) + numerical */
let gaussExact = 0, gaussNumerical = 0;
if (gaussOn && this.mode !== 'B') {
const g = this._gauss;
const eps0inv = 1 / (4 * Math.PI * this.K_E); // 1/ε₀ in visual units
for (const s of this.sources) {
if (s.kind !== 'charge') continue;
if (Math.hypot(s.x - g.x, s.y - g.y) < g.r) gaussExact += s.q;
}
gaussExact *= eps0inv; // Gauss: Φ = q_enc / ε₀
/* numerical line integral ∮ E·n ds */
const N = 64;
for (let k = 0; k < N; k++) {
const a = (k / N) * Math.PI * 2;
const px = g.x + g.r * Math.cos(a), py = g.y + g.r * Math.sin(a);
const { ex, ey } = this._eField(px, py);
const nx = Math.cos(a), ny = Math.sin(a); // outward normal
gaussNumerical += (ex * nx + ey * ny) * g.r * (2 * Math.PI / N);
}
}
/* Rod EMF */
let rodEMF = 0, rodV = 0, rodAvgB = 0;
if (rodOn) {
const r = this._rodEMF();
rodEMF = r.emf; rodV = r.v; rodAvgB = r.avgB;
}
return {
total: this.sources.length,
charges: charges.length, pos, neg,
wires: wires.length, out, inn,
particleOn: this.particleOn,
condOn, fluxOn, Fz, flux,
condOn, fluxOn, gaussOn, rodOn, Fz, flux,
gaussExact, gaussNumerical,
rodEMF, rodV, rodAvgB,
cursorE: this._cursorE ? this._cursorE.mag.toFixed(0) : '—',
cursorV: this._cursorE ? this._cursorE.v.toFixed(0) : '—',
cursorB: this._cursorB ? this._cursorB.mag.toFixed(0) : '—',
@@ -519,6 +696,18 @@ class EMFieldSim {
return Math.hypot(p.x - this._flux.x, p.y - this._flux.y) < this._flux.r + 12;
};
const hitGauss = p => {
if (!this._gauss.on) return false;
return Math.hypot(p.x - this._gauss.x, p.y - this._gauss.y) < this._gauss.r + 12;
};
const hitRod = p => {
if (!this._rod.on) return false;
const { x1, y1, x2, y2 } = this._rod;
const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
return Math.hypot(p.x - mx, p.y - my) < Math.hypot(x2 - x1, y2 - y1) / 2 + 14;
};
let _condDragOffset = null;
c.addEventListener('mousedown', e => {
@@ -543,6 +732,19 @@ class EMFieldSim {
c.style.cursor = 'grabbing'; return;
}
if (hitGauss(p)) {
this._gauss._dragging = true;
c.style.cursor = 'grabbing'; return;
}
if (hitRod(p)) {
const rod = this._rod;
rod._dragging = true;
rod._dragOffX = p.x - (rod.x1 + rod.x2) / 2;
rod._dragOffY = p.y - (rod.y1 + rod.y2) / 2;
c.style.cursor = 'grabbing'; return;
}
const i = hitSource(p);
if (i >= 0) { this._drag = i; c.style.cursor = 'grabbing'; }
});
@@ -588,6 +790,20 @@ class EMFieldSim {
this.draw(); return;
}
if (this._gauss._dragging) {
this._gauss.x = p.x; this._gauss.y = p.y;
this.draw(); return;
}
if (this._rod._dragging) {
const rod = this._rod;
const cx = p.x - rod._dragOffX, cy = p.y - rod._dragOffY;
const hLx = (rod.x2 - rod.x1) / 2, hLy = (rod.y2 - rod.y1) / 2;
rod.x1 = cx - hLx; rod.y1 = cy - hLy;
rod.x2 = cx + hLx; rod.y2 = cy + hLy;
this.draw(); return;
}
if (this._drag !== null) {
this.sources[this._drag].x = p.x;
this.sources[this._drag].y = p.y;
@@ -597,7 +813,7 @@ class EMFieldSim {
const i = hitSource(p);
const ch = hitCond(p);
const fh = hitFlux(p);
const fh = hitFlux(p) || hitGauss(p) || hitRod(p);
this._hovered = i >= 0 ? i : null;
c.style.cursor = (i >= 0 || ch !== null || fh) ? 'grab' : 'crosshair';
this.draw();
@@ -614,6 +830,12 @@ class EMFieldSim {
if (this._flux._dragging) {
this._flux._dragging = false; c.style.cursor = 'crosshair'; return;
}
if (this._gauss._dragging) {
this._gauss._dragging = false; c.style.cursor = 'crosshair'; return;
}
if (this._rod._dragging) {
this._rod._dragging = false; c.style.cursor = 'crosshair'; return;
}
if (this._drag !== null) {
this._invalidateAll();
this._drag = null; c.style.cursor = 'crosshair';
@@ -622,7 +844,7 @@ class EMFieldSim {
/* click on empty canvas — add source based on mode */
if (!moved && e.button === 0 &&
hitSource(p) < 0 && hitCond(p) === null && !hitFlux(p)) {
hitSource(p) < 0 && hitCond(p) === null && !hitFlux(p) && !hitGauss(p) && !hitRod(p)) {
if (this.mode === 'E') {
this.addCharge(p.x, p.y, this.addSign);
} else if (this.mode === 'B') {
@@ -686,6 +908,18 @@ class EMFieldSim {
this._drag = null;
if (this.onUpdate) this.onUpdate(this.info());
}, { passive: false });
/* arrow-key control for rod */
document.addEventListener('keydown', e => {
if (!this._rod.on) return;
if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)) {
e.preventDefault();
this._rod._keys[e.key] = true;
}
});
document.addEventListener('keyup', e => {
delete this._rod._keys[e.key];
});
}
/* ──────────────────────────────
@@ -729,6 +963,8 @@ class EMFieldSim {
/* overlays */
if (this._flux.on && this.mode !== 'E') this._drawFlux(ctx);
if (this._cond.on && this.mode !== 'E') this._drawConductor(ctx);
if (this._gauss.on && this.mode !== 'B') this._drawGauss(ctx);
if (this._rod.on && this.mode !== 'E') this._drawRod(ctx);
if (this._particle) this._drawParticle(ctx);
/* sources */
@@ -1238,6 +1474,129 @@ class EMFieldSim {
ctx.restore();
}
/* ── Gauss surface (electric flux) ── */
_drawGauss(ctx) {
const g = this._gauss;
/* compute enclosed charge and numerical flux */
const eps0inv = 1 / (4 * Math.PI * this.K_E);
let qEnc = 0;
for (const s of this.sources) {
if (s.kind !== 'charge') continue;
if (Math.hypot(s.x - g.x, s.y - g.y) < g.r) qEnc += s.q;
}
const phiExact = qEnc * eps0inv;
/* draw enclosed charge halo */
ctx.save();
for (const s of this.sources) {
if (s.kind !== 'charge') continue;
if (Math.hypot(s.x - g.x, s.y - g.y) < g.r) {
ctx.beginPath(); ctx.arc(s.x, s.y, 26, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(52,211,153,0.55)'; ctx.lineWidth = 3;
ctx.shadowColor = '#34d399'; ctx.shadowBlur = 12; ctx.stroke();
}
}
ctx.restore();
/* background fill */
ctx.save();
const grad = ctx.createRadialGradient(g.x, g.y, 0, g.x, g.y, g.r);
const a = Math.min(0.35, Math.abs(phiExact) * 0.008 + 0.05);
grad.addColorStop(0, `rgba(52,211,153,${a})`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(g.x, g.y, g.r, 0, Math.PI * 2); ctx.fill();
/* dashed circle with flowing dash-offset to suggest surface motion */
ctx.setLineDash([10, 6]);
ctx.strokeStyle = 'rgba(52,211,153,0.85)'; ctx.lineWidth = 2;
ctx.shadowColor = '#34d399'; ctx.shadowBlur = 10;
ctx.beginPath(); ctx.arc(g.x, g.y, g.r, 0, Math.PI * 2); ctx.stroke();
ctx.setLineDash([]);
ctx.shadowBlur = 0;
/* normal arrows on circle */
const nArr = 12;
ctx.strokeStyle = 'rgba(52,211,153,0.5)'; ctx.fillStyle = 'rgba(52,211,153,0.7)'; ctx.lineWidth = 1.2;
for (let k = 0; k < nArr; k++) {
const a2 = (k / nArr) * Math.PI * 2;
const ex = Math.cos(a2), ey = Math.sin(a2);
const rx = g.x + g.r * ex, ry = g.y + g.r * ey;
const len = phiExact !== 0 ? (phiExact > 0 ? 14 : -14) : 10;
const x2 = rx + ex * len, y2 = ry + ey * len;
ctx.beginPath(); ctx.moveTo(rx, ry); ctx.lineTo(x2, y2); ctx.stroke();
const ang = Math.atan2(ey, ex);
ctx.save(); ctx.translate(x2, y2); ctx.rotate(ang);
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-5, -3); ctx.lineTo(-5, 3);
ctx.closePath(); ctx.fill(); ctx.restore();
}
/* label */
ctx.font = 'bold 11px Manrope, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
ctx.fillStyle = '#34d399'; ctx.shadowColor = '#34d399'; ctx.shadowBlur = 6;
const signStr = phiExact >= 0 ? '+' : '';
ctx.fillText('Φₑ = ' + signStr + phiExact.toFixed(3) + ' (точн.)', g.x, g.y + g.r + 6);
ctx.font = '10px Manrope, sans-serif'; ctx.fillStyle = 'rgba(52,211,153,0.7)'; ctx.shadowBlur = 3;
ctx.fillText('qₑₙₙ = ' + qEnc.toFixed(1) + ' | перетащи', g.x, g.y + g.r + 20);
ctx.restore();
}
/* ── motional EMF rod ── */
_drawRod(ctx) {
const rod = this._rod;
const Lx = rod.x2 - rod.x1, Ly = rod.y2 - rod.y1;
const L = Math.hypot(Lx, Ly);
if (L < 2) return;
const { emf, avgB, v } = this._rodEMF();
const mx = (rod.x1 + rod.x2) / 2, my = (rod.y1 + rod.y2) / 2;
ctx.save();
/* velocity arrow */
if (v > 0.5) {
const spd = Math.min(50, v * 0.5);
const vx = rod.vx / v, vy = rod.vy / v;
const ax2 = mx + vx * spd, ay2 = my + vy * spd;
ctx.strokeStyle = '#a78bfa'; ctx.lineWidth = 2; ctx.shadowColor = '#a78bfa'; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.moveTo(mx, my); ctx.lineTo(ax2, ay2); ctx.stroke();
const ang = Math.atan2(vy, vx);
ctx.save(); ctx.translate(ax2, ay2); ctx.rotate(ang);
ctx.fillStyle = '#a78bfa';
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(-8,-4); ctx.lineTo(-8,4); ctx.closePath(); ctx.fill();
ctx.restore();
ctx.font = '10px Manrope'; ctx.fillStyle = '#a78bfa';
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
ctx.fillText('v', ax2, ay2 - 6);
}
/* rod itself */
ctx.shadowColor = '#f59e0b'; ctx.shadowBlur = 16;
ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 5; ctx.globalAlpha = 0.35; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(rod.x1, rod.y1); ctx.lineTo(rod.x2, rod.y2); ctx.stroke();
ctx.globalAlpha = 1; ctx.shadowBlur = 8; ctx.lineWidth = 3.5;
ctx.strokeStyle = '#f59e0b';
ctx.beginPath(); ctx.moveTo(rod.x1, rod.y1); ctx.lineTo(rod.x2, rod.y2); ctx.stroke();
/* endpoints */
[[rod.x1,rod.y1],[rod.x2,rod.y2]].forEach(([ex,ey]) => {
ctx.beginPath(); ctx.arc(ex, ey, 7, 0, Math.PI * 2);
ctx.fillStyle = '#f59e0b'; ctx.shadowBlur = 10; ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
});
/* EMF label */
const perpX = -Ly / L, perpY = Lx / L;
ctx.shadowBlur = 6; ctx.shadowColor = '#f59e0b';
ctx.font = 'bold 11px Manrope'; ctx.fillStyle = '#f59e0b';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('ε = ' + emf.toFixed(4) + ' (ед)', mx + perpX * 26, my + perpY * 26);
ctx.font = '10px Manrope'; ctx.fillStyle = 'rgba(245,158,11,0.75)'; ctx.shadowBlur = 3;
ctx.fillText('|B|̲ = ' + avgB.toFixed(1) + ' v = ' + v.toFixed(1), mx + perpX * 26, my + perpY * 40);
ctx.fillText('← ↑ → ↓ — перемещение', mx, my - L / 2 - 14);
ctx.restore();
}
/* ── particle ── */
_drawParticle(ctx) {
const p = this._particle;
@@ -1510,6 +1869,44 @@ function emFluxToggle(rowEl) {
function emPresetE(name) { if (emSim) emSim.presetE(name); }
function emPresetB(name) { if (emSim) emSim.presetB(name); }
function emGaussToggle(rowEl) {
if (!emSim) return;
emSim.toggleGauss();
const on = emSim._gauss.on;
rowEl.classList.toggle('active', on);
const tog = rowEl.querySelector('.tri-toggle');
if (tog) {
tog.style.background = on ? 'var(--violet)' : 'rgba(255,255,255,0.12)';
const dot = tog.querySelector('span');
if (dot) dot.style.marginLeft = on ? '14px' : '2px';
}
const block = document.getElementById('em-gauss-r-block');
if (block) block.style.display = on ? '' : 'none';
_emUpdateUI(emSim.info());
}
function emGaussRChange() {
if (!emSim) return;
const r = parseFloat(document.getElementById('sl-emGaussR').value);
const lbl = document.getElementById('em-gaussR-val');
if (lbl) lbl.textContent = Math.round(r) + ' пкс';
emSim.setGaussR(r);
}
function emRodToggle(rowEl) {
if (!emSim) return;
emSim.toggleRod();
const on = emSim._rod.on;
rowEl.classList.toggle('active', on);
const tog = rowEl.querySelector('.tri-toggle');
if (tog) {
tog.style.background = on ? 'var(--violet)' : 'rgba(255,255,255,0.12)';
const dot = tog.querySelector('span');
if (dot) dot.style.marginLeft = on ? '14px' : '2px';
}
_emUpdateUI(emSim.info());
}
function _emUpdateUI(info) {
if (!info) return;
const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
@@ -1527,7 +1924,7 @@ function _emUpdateUI(info) {
const fEl = document.getElementById('embar-ampere');
if (fEl) {
if (info.condOn && info.Fz !== 0) {
fEl.textContent = (info.Fz > 0 ? ' ' : ' ') + Math.abs(info.Fz).toFixed(3);
fEl.textContent = (info.Fz > 0 ? '(+) ' : '(-) ') + Math.abs(info.Fz).toFixed(3);
fEl.style.color = '#fbbf24';
} else {
fEl.textContent = '—'; fEl.style.color = '#fbbf24';
@@ -1538,4 +1935,27 @@ function _emUpdateUI(info) {
if (info.fluxOn) { phEl.textContent = info.flux.toExponential(2) + ' Вб'; phEl.style.color = '#34d399'; }
else { phEl.textContent = '—'; phEl.style.color = '#34d399'; }
}
/* Gauss surface stats */
const gEl = document.getElementById('embar-gauss');
if (gEl) {
if (info.gaussOn) {
const sign = info.gaussExact >= 0 ? '+' : '';
gEl.textContent = sign + info.gaussExact.toFixed(3);
gEl.style.color = '#34d399';
} else {
gEl.textContent = '—'; gEl.style.color = '#34d399';
}
}
/* Rod EMF stats */
const rEl = document.getElementById('embar-rod');
if (rEl) {
if (info.rodOn) {
rEl.textContent = info.rodEMF.toFixed(4) + ' ед';
rEl.style.color = '#f59e0b';
} else {
rEl.textContent = '—'; rEl.style.color = '#f59e0b';
}
}
}