'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);