Files
Learn_System/backend/scripts/patch_interfsim.js
T
Maxim Dolgolyov 5381679c68 chore: консолидация незакоммиченной работы (биохимия + System Health + lab/textbooks)
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов
учебника «Химия 7» (migration 046, chemistry_7_*.html, chem7_svg.js, тест —
оставлены незакоммиченными по запросу).

Включает: модуль биохимии (ядро BIO, 3D VSEPR, химдвижок, баланс, challenges,
пути из БД), System Health Level 1 (вердикт/мониторинг), а также frontend-
страницы и lab/textbooks-правки параллельной сессии.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:12:55 +03:00

410 lines
16 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const targetFile = path.join(__dirname, '../../frontend/js/labs/opticsbench.js');
const ifSimCode = `/* ─────────────────────────────────────────────────────────────
4d. INTERFERENCE SIM — Newton's rings / Thin film / Polarization
Agent C — additive only, class InterferenceSim
─────────────────────────────────────────────────────────────*/
class InterferenceSim {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.W = 0; this.H = 0;
this.subMode = 'newton';
// Newton rings
this.nR = 200;
this.nNmax = 12;
// Thin film
this.tfT = 400;
this.tfN = 1.33;
this.tfTheta = 0;
this.tfPreset = 'soap';
// Polarization
this.polTheta = 45;
this.polSrc = 'unpolarized';
this._polTick = 0;
this._polRaf = null;
this.onUpdate = null;
new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement || canvas);
}
fit() {
const p = this.canvas.parentElement;
if (!p) return;
const r = p.getBoundingClientRect();
this.W = this.canvas.width = r.width || p.offsetWidth || 600;
this.H = this.canvas.height = r.height || p.offsetHeight || 400;
}
setSubMode(sm) {
this.subMode = sm;
if (sm === 'polarization') {
this._polStart();
} else {
this._polStop();
}
this.draw();
if (this.onUpdate) this.onUpdate();
}
/* ── Newton Rings ──────────────────────────────────────── */
_drawNewton() {
const { ctx, W, H } = this;
const nm = window._obWavelength || 550;
const R = this.nR;
const nMax = this.nNmax;
const white = window._obWhiteLight;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#08081a';
ctx.fillRect(0, 0, W, H);
const topH = Math.floor(H * 0.60);
const cx = W / 2, cy = topH / 2;
const maxR_mm = Math.sqrt(nMax * nm * 1e-6 * R);
const scale = Math.min(cx * 0.85, cy * 0.85) / (maxR_mm || 1);
for (let n = nMax; n >= 0; n--) {
const lambdas = white ? [420, 470, 510, 550, 590, 620, 680] : [nm];
for (const lam of lambdas) {
const rDark = Math.sqrt(n * lam * 1e-6 * R) * scale;
const rBright = Math.sqrt((n + 0.5) * lam * 1e-6 * R) * scale;
if (rDark > 0.5) {
ctx.beginPath();
ctx.arc(cx, cy, rDark, 0, Math.PI * 2);
ctx.strokeStyle = white
? wavelengthToRGB(lam).replace(')', ',0.5)').replace('rgb', 'rgba')
: '#000000';
ctx.lineWidth = white ? 1.2 : 1.5;
ctx.stroke();
}
if (rBright > 0.5) {
const al = white ? 0.22 : 0.55;
ctx.beginPath();
ctx.arc(cx, cy, rBright, 0, Math.PI * 2);
ctx.strokeStyle = wavelengthToRGB(lam).replace(')', ',' + al + ')').replace('rgb', 'rgba');
ctx.lineWidth = 2.5;
ctx.stroke();
}
}
}
ctx.beginPath(); ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fillStyle = '#000000'; ctx.fill();
if (window.LabFX && LabFX.glow && !white) {
const r1b = Math.sqrt(0.5 * nm * 1e-6 * R) * scale;
LabFX.glow.drawGlow(ctx, cx, cy, r1b, wavelengthToRGB(nm), 18);
}
ctx.beginPath(); ctx.arc(cx, cy, maxR_mm * scale * 1.05, 0, Math.PI * 2);
ctx.strokeStyle = '#334455'; ctx.lineWidth = 1; ctx.stroke();
const crossY0 = topH + 8;
const crossH = H - crossY0 - 40;
if (crossH < 30) return;
ctx.fillStyle = '#0d0d20';
ctx.fillRect(0, crossY0, W, crossH + 36);
const glassY = crossY0 + crossH - 10;
ctx.fillStyle = '#1a3a5c';
ctx.fillRect(cx - maxR_mm * scale * 1.1, glassY, maxR_mm * scale * 2.2, 10);
const sagitta = (maxR_mm * maxR_mm) / (2 * R);
const sagPx = sagitta * scale;
ctx.beginPath();
ctx.ellipse(cx, glassY - 1 - sagPx, maxR_mm * scale * 1.1, sagPx + 6, 0, 0, Math.PI);
ctx.fillStyle = 'rgba(100,180,255,0.15)'; ctx.fill();
ctx.strokeStyle = '#4499cc'; ctx.lineWidth = 1.5; ctx.stroke();
for (let n = 0; n <= nMax; n++) {
const rD = Math.sqrt(n * nm * 1e-6 * R) * scale;
if (rD < 1) continue;
ctx.beginPath();
ctx.moveTo(cx + rD, glassY); ctx.lineTo(cx + rD, glassY + 8);
ctx.moveTo(cx - rD, glassY); ctx.lineTo(cx - rD, glassY + 8);
ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();
}
ctx.font = '600 11px monospace'; ctx.fillStyle = '#667788'; ctx.textAlign = 'center';
ctx.fillText('Cross-section', cx, crossY0 + 14);
const r1d = Math.sqrt(nm * 1e-6 * R).toFixed(3);
this._drawHUD(ctx, W, H,
'r1 = sqrt(lam*R) = ' + r1d + ' mm | R=' + R + 'mm | lam=' + nm + 'nm');
}
/* ── Thin Film ─────────────────────────────────────────── */
_thinFilmColor(t_nm, n_film, theta_deg) {
const sinR = Math.sin(theta_deg * Math.PI / 180) / n_film;
const cosR = Math.sqrt(Math.max(0, 1 - sinR * sinR));
const opd = 2 * n_film * t_nm * cosR;
let rS = 0, gS = 0, bS = 0;
for (let lam = 380; lam <= 780; lam += 5) {
const phase = Math.PI * opd / lam;
const I = Math.cos(phase) * Math.cos(phase);
const rgb = wavelengthToRGB(lam);
const m = rgb.match(/\d+/g);
if (!m) continue;
rS += I * +m[0]; gS += I * +m[1]; bS += I * +m[2];
}
const sc = 255 / Math.max(rS, gS, bS, 1);
return 'rgb(' + Math.round(rS * sc) + ',' + Math.round(gS * sc) + ',' + Math.round(bS * sc) + ')';
}
_drawThinFilm() {
const { ctx, W, H } = this;
const t = this.tfT;
const nf = this.tfN;
const theta = this.tfTheta;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#08081a';
ctx.fillRect(0, 0, W, H);
const midY = H * 0.40;
const filmH = Math.max(28, H * 0.12);
const margin = W * 0.10;
const ang = theta * Math.PI / 180;
const skew = Math.tan(ang) * filmH * 0.5;
const grad = ctx.createLinearGradient(margin, 0, W - margin, 0);
for (let i = 0; i <= 20; i++) {
const frac = i / 20;
grad.addColorStop(frac, this._thinFilmColor(t * (0.3 + 0.7 * frac), nf, theta));
}
ctx.save();
ctx.beginPath();
ctx.moveTo(margin - skew, midY - filmH / 2);
ctx.lineTo(W - margin - skew, midY - filmH / 2);
ctx.lineTo(W - margin + skew, midY + filmH / 2);
ctx.lineTo(margin + skew, midY + filmH / 2);
ctx.closePath();
ctx.fillStyle = grad; ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1; ctx.stroke();
ctx.restore();
ctx.font = '700 11px sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.textAlign = 'center';
ctx.fillText('t=' + t + 'nm n=' + nf.toFixed(2), W / 2, midY);
const ax2 = W * 0.25, ay2 = midY - filmH / 2;
const ax1 = ax2 - Math.cos(ang) * 40, ay1 = ay2 - Math.sin(ang) * 40 - 20;
ctx.beginPath(); ctx.moveTo(ax1, ay1); ctx.lineTo(ax2, ay2);
ctx.strokeStyle = '#8ab4e8'; ctx.lineWidth = 1.5; ctx.stroke();
const col = this._thinFilmColor(t, nf, theta);
ctx.beginPath(); ctx.moveTo(ax2, ay2); ctx.lineTo(ax2 - Math.cos(ang) * 40, ay1);
ctx.strokeStyle = col; ctx.lineWidth = 2; ctx.stroke();
const dx2 = Math.sin(ang) * filmH / nf;
ctx.beginPath();
ctx.moveTo(ax2 + dx2, ay2 + filmH);
ctx.lineTo(ax2 + dx2 - Math.cos(ang) * 40, ay1 + filmH - 20);
ctx.strokeStyle = col; ctx.lineWidth = 2;
ctx.setLineDash([4, 3]); ctx.stroke(); ctx.setLineDash([]);
const tvX0 = W * 0.55, tvW2 = W * 0.38;
const tvY0 = H * 0.05, tvH2 = H * 0.60;
ctx.fillStyle = '#0d0d22'; ctx.strokeStyle = '#2a2a4a'; ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(tvX0, tvY0, tvW2, tvH2, 8);
else ctx.rect(tvX0, tvY0, tvW2, tvH2);
ctx.fill(); ctx.stroke();
ctx.font = '600 10px sans-serif'; ctx.fillStyle = '#555'; ctx.textAlign = 'center';
ctx.fillText('Top view', tvX0 + tvW2 / 2, tvY0 + 14);
const tvRows = 28, tvCols = 36;
const cW = tvW2 / tvCols, cH = (tvH2 - 20) / tvRows;
for (let r = 0; r < tvRows; r++) {
for (let c = 0; c < tvCols; c++) {
ctx.fillStyle = this._thinFilmColor(t * (0.5 + c / tvCols), nf, theta * (r / tvRows));
ctx.fillRect(tvX0 + c * cW, tvY0 + 20 + r * cH, cW + 0.5, cH + 0.5);
}
}
const sinR2 = Math.sin(ang) / nf;
const cosR2 = Math.sqrt(Math.max(0, 1 - sinR2 * sinR2));
const opd2 = (2 * nf * t * cosR2).toFixed(0);
this._drawHUD(ctx, W, H,
'2nt*cos(th_r)=' + opd2 + 'nm | t=' + t + 'nm n=' + nf.toFixed(2) + ' th=' + theta + 'deg');
}
/* ── Polarization ──────────────────────────────────────── */
_polStart() {
if (this._polRaf) return;
const loop = () => { this._polTick++; this.draw(); this._polRaf = requestAnimationFrame(loop); };
this._polRaf = requestAnimationFrame(loop);
}
_polStop() {
if (this._polRaf) { cancelAnimationFrame(this._polRaf); this._polRaf = null; }
}
_drawPolarization() {
const { ctx, W, H } = this;
const theta = this.polTheta * Math.PI / 180;
const I_rel = Math.cos(theta) * Math.cos(theta);
const tick = this._polTick;
const white = window._obWhiteLight;
const nm = window._obWavelength || 550;
const beamCol = white ? '#ffffff' : wavelengthToRGB(nm);
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#08081a';
ctx.fillRect(0, 0, W, H);
const axisY = H * 0.45;
const stH = H * 0.38;
const st = [
{ x: W * 0.12, label: 'Источник', isFilter: false },
{ x: W * 0.38, label: 'Поляризатор P1', isFilter: true, angle: 0 },
{ x: W * 0.64, label: 'Анализатор P2', isFilter: true, angle: this.polTheta },
{ x: W * 0.88, label: 'Детектор', isFilter: false },
];
ctx.beginPath();
ctx.moveTo(st[0].x - 20, axisY); ctx.lineTo(st[3].x + 20, axisY);
ctx.strokeStyle = '#1a1a35'; ctx.lineWidth = 1; ctx.stroke();
const segs = [
{ x0: st[0].x, x1: st[1].x, amp: 1, unpol: this.polSrc === 'unpolarized', ang: 0 },
{ x0: st[1].x, x1: st[2].x, amp: 1, unpol: false, ang: 0 },
{ x0: st[2].x, x1: st[3].x, amp: I_rel, unpol: false, ang: this.polTheta },
];
for (const seg of segs) {
const nA = 20;
const sdx = (seg.x1 - seg.x0) / nA;
for (let i = 0; i <= nA; i++) {
const bx = seg.x0 + i * sdx;
const phase = (bx * 0.08 - tick * 0.04) % (Math.PI * 2);
const bAmp = stH * 0.28 * seg.amp;
if (seg.unpol) {
for (let d = 0; d < 4; d++) {
const a = d * Math.PI / 4;
const oy = Math.sin(phase + d * 0.7) * bAmp;
ctx.beginPath(); ctx.moveTo(bx, axisY);
ctx.lineTo(bx + oy * Math.sin(a) * 0.25, axisY + oy * Math.cos(a));
ctx.strokeStyle = 'rgba(200,200,255,0.22)'; ctx.lineWidth = 1; ctx.stroke();
}
} else {
const oy = Math.sin(phase) * bAmp;
const a = seg.ang * Math.PI / 180;
const py = oy * Math.cos(a), px = oy * Math.sin(a) * 0.35;
ctx.beginPath();
ctx.moveTo(bx - px, axisY - py); ctx.lineTo(bx + px, axisY + py);
ctx.strokeStyle = (I_rel < 0.01 && seg.amp < 0.5)
? 'rgba(80,80,120,0.5)'
: beamCol.replace(')', ',0.75)').replace('rgb', 'rgba');
ctx.lineWidth = 1.5; ctx.stroke();
if (i % 3 === 0 && bAmp > 2) {
ctx.beginPath(); ctx.arc(bx + px, axisY + py, 2, 0, Math.PI * 2);
ctx.fillStyle = beamCol; ctx.fill();
}
}
}
}
for (const s of st) {
if (!s.isFilter) continue;
const a = s.angle * Math.PI / 180;
ctx.save(); ctx.translate(s.x, axisY);
ctx.fillStyle = 'rgba(80,120,200,0.18)';
ctx.fillRect(-4, -stH / 2, 8, stH);
ctx.strokeStyle = '#4466aa'; ctx.lineWidth = 1.5; ctx.strokeRect(-4, -stH / 2, 8, stH);
const axLen = stH * 0.45;
ctx.beginPath();
ctx.moveTo(-Math.sin(a) * axLen, -Math.cos(a) * axLen);
ctx.lineTo( Math.sin(a) * axLen, Math.cos(a) * axLen);
ctx.strokeStyle = '#7aaeff'; ctx.lineWidth = 2; ctx.stroke();
ctx.restore();
ctx.font = '700 10px monospace'; ctx.fillStyle = '#7aaeff'; ctx.textAlign = 'center';
ctx.fillText(s.angle + 'deg', s.x, axisY + stH / 2 + 14);
}
for (const s of st) {
ctx.font = '600 10px sans-serif'; ctx.fillStyle = '#667788'; ctx.textAlign = 'center';
ctx.fillText(s.label, s.x, axisY - stH / 2 - 8);
}
const barX = W * 0.91, barW = 16;
const barY0 = axisY - stH / 2;
ctx.fillStyle = '#111122'; ctx.fillRect(barX, barY0, barW, stH);
const fillH2 = stH * I_rel;
if (fillH2 > 0) {
const bg = ctx.createLinearGradient(barX, barY0 + stH - fillH2, barX, barY0 + stH);
bg.addColorStop(0, beamCol); bg.addColorStop(1, 'rgba(0,0,0,0.2)');
ctx.fillStyle = bg; ctx.fillRect(barX, barY0 + stH - fillH2, barW, fillH2);
}
ctx.strokeStyle = '#334455'; ctx.lineWidth = 1; ctx.strokeRect(barX, barY0, barW, stH);
ctx.font = '600 9px monospace'; ctx.fillStyle = '#aaaaaa'; ctx.textAlign = 'center';
ctx.fillText('I', barX + barW / 2, barY0 - 5);
if (this.polTheta >= 88) {
ctx.font = '700 13px sans-serif'; ctx.fillStyle = '#EF476F'; ctx.textAlign = 'center';
ctx.fillText('Полное гашение', W / 2, H * 0.85);
}
ctx.font = '10px sans-serif'; ctx.fillStyle = '#444466'; ctx.textAlign = 'right';
ctx.fillText('Угол Брюстера: отражённый свет поляризован (см. Преломление)', W - 10, H - 10);
const pct = (I_rel * 100).toFixed(1);
this._drawHUD(ctx, W, H,
'I/I0=cos2(th)=cos2(' + this.polTheta + 'deg)=' + I_rel.toFixed(3) + ' (' + pct + '%)');
}
_drawHUD(ctx, W, H, text) {
const pad = 8, fs = 11;
ctx.font = '600 ' + fs + 'px monospace';
const tw = ctx.measureText(text).width;
const bx = (W - tw) / 2 - pad, by = H - 32;
const bw = tw + pad * 2, bh = fs + pad * 2;
ctx.fillStyle = 'rgba(10,10,30,0.82)';
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, 5);
else ctx.rect(bx, by, bw, bh);
ctx.fill();
ctx.fillStyle = '#c8d8ff';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.fillText(text, bx + pad, by + bh / 2);
ctx.textBaseline = 'alphabetic';
}
draw() {
if (this.subMode === 'newton') this._drawNewton();
else if (this.subMode === 'thinfilm') this._drawThinFilm();
else if (this.subMode === 'polarization') this._drawPolarization();
}
}
`;
const src = fs.readFileSync(targetFile, 'utf-8');
const markerStr = '4c. SPECTROMETER PANEL';
const markerIdx = src.indexOf(markerStr);
if (markerIdx < 0) {
console.error('ERROR: marker not found');
process.exit(1);
}
const insertIdx = src.lastIndexOf('/*', markerIdx);
if (insertIdx < 0) {
console.error('ERROR: comment start not found');
process.exit(1);
}
// Check InterferenceSim not already present
if (src.indexOf('class InterferenceSim') >= 0) {
console.log('InterferenceSim already present — skipping JS insertion');
process.exit(0);
}
const result = src.slice(0, insertIdx) + ifSimCode + src.slice(insertIdx);
fs.writeFileSync(targetFile, result, 'utf-8');
console.log('JS insertion OK. New size:', result.length);