import sys, os src = os.path.join(os.path.dirname(__file__), '../../frontend/js/labs/opticsbench.js') with open(src, 'r', encoding='utf-8') as f: content = f.read() # ───────────────────────────────────────────────────────────────────────────── # PATCH 1: Insert _drawRaysAnimated + _drawArrowLabels before _bindEvents # in ThinLensSim # ───────────────────────────────────────────────────────────────────────────── MARKER_BIND = ' _bindEvents() {\n const cv = this.canvas;\n const getPos = (e) => {' idx = content.find(MARKER_BIND) if idx == -1: print('ERROR: _bindEvents marker not found'); sys.exit(1) NEW_METHODS_1 = ( ' /* === _drawRaysAnimated: principal rays with per-ray progress === */\n' ' _drawRaysAnimated(ctx, lx, ay, d, h, f, dPrime, hPrime) {\n' ' const T = this._rayAnimT;\n' ' if (T[0] >= 1 && T[1] >= 1 && T[2] >= 1) { this._drawRays(ctx, lx, ay, d, h, f, dPrime, hPrime); return; }\n' ' const objX = lx - d, objY = ay - h;\n' ' const hasImage = dPrime !== null && isFinite(dPrime);\n' ' const isVirtual = hasImage && dPrime < 0;\n' ' const COLORS = [\'#06D6E0\', \'#7BF5A4\', \'#FFD166\'];\n' ' ctx.lineWidth = 1.8;\n' ' const lerp = (a, b, t) => a + (b - a) * Math.min(1, Math.max(0, t));\n' ' const drawPts = (color, pts, t) => {\n' ' if (t <= 0 || pts.length < 2) return;\n' ' const totalLen = pts.reduce((s, p, i) => i === 0 ? 0 : s + Math.hypot(p[0]-pts[i-1][0], p[1]-pts[i-1][1]), 0);\n' ' const target = totalLen * t;\n' ' const draw = () => {\n' ' ctx.strokeStyle = color; ctx.setLineDash([]);\n' ' ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]);\n' ' let drawn = 0;\n' ' for (let i = 1; i < pts.length; i++) {\n' ' const segLen = Math.hypot(pts[i][0]-pts[i-1][0], pts[i][1]-pts[i-1][1]);\n' ' if (drawn + segLen <= target) { ctx.lineTo(pts[i][0], pts[i][1]); drawn += segLen; }\n' ' else { const fr = segLen > 0 ? (target - drawn) / segLen : 0; ctx.lineTo(lerp(pts[i-1][0], pts[i][0], fr), lerp(pts[i-1][1], pts[i][1], fr)); break; }\n' ' }\n' ' ctx.stroke();\n' ' };\n' ' if (window.LabFX) LabFX.glow.drawGlow(ctx, draw, { color, intensity: 10 });\n' ' else draw();\n' ' };\n' ' const FAR = lx + 360;\n' ' const imgX = hasImage ? lx + dPrime : null, imgY = hasImage ? ay - hPrime : null;\n' ' // Ray 1: parallel to axis -> through F\'\n' ' if (T[0] > 0) {\n' ' let pts;\n' ' if (!hasImage) { pts = [[objX, objY], [lx, objY], [FAR, objY]]; }\n' ' else if (!isVirtual) { pts = [[objX, objY], [lx, objY], [imgX, imgY]]; }\n' ' else { const s = (objY - ay) / f; pts = [[objX, objY], [lx, objY], [FAR, objY + s*(FAR-lx)]]; }\n' ' drawPts(COLORS[0], pts, T[0]);\n' ' }\n' ' // Ray 2: through optical center (straight)\n' ' if (T[1] > 0) {\n' ' const s = (objY - ay) / (objX - lx);\n' ' drawPts(COLORS[1], [[objX, objY], [FAR, ay + s*(FAR-lx)]], T[1]);\n' ' }\n' ' // Ray 3: through front focus F -> parallel after lens\n' ' if (T[2] > 0) {\n' ' const fx = lx - f, s = (objY - ay) / (objX - fx);\n' ' const hitY = objY + s * (lx - objX);\n' ' const endX = hasImage && !isVirtual ? Math.max(imgX + 60, FAR) : FAR;\n' ' drawPts(COLORS[2], [[objX, objY], [lx, hitY], [endX, hitY]], T[2]);\n' ' }\n' ' }\n' '\n' ' /* === Arrow labels: h_o, h_i, magnification Gamma === */\n' ' _drawArrowLabels(ctx, lx, ay, d, h, dPrime, hPrime) {\n' ' const objX = lx - d;\n' ' ctx.font = \'11px Manrope, system-ui, sans-serif\'; ctx.textBaseline = \'middle\';\n' ' ctx.fillStyle = \'rgba(155,93,229,0.85)\'; ctx.textAlign = \'right\';\n' ' ctx.fillText(\'ho=\' + h.toFixed(0), objX - 6, ay - h / 2);\n' ' if (dPrime !== null && isFinite(dPrime)) {\n' ' const imgX = lx + dPrime, isVirtual = dPrime < 0;\n' ' const M = -dPrime / d;\n' ' const Gstr = isFinite(M) ? (M >= 0 ? \'+\' : \'\') + M.toFixed(2) : \'---\';\n' ' const imgColor = isVirtual ? \'rgba(255,133,162,0.85)\' : \'rgba(6,214,224,0.85)\';\n' ' ctx.fillStyle = imgColor; ctx.textAlign = \'left\';\n' ' ctx.fillText("hi=" + Math.abs(hPrime).toFixed(0), imgX + 6, ay - hPrime / 2);\n' ' ctx.fillStyle = \'#FFD166\'; ctx.textAlign = \'center\';\n' ' ctx.fillText(\'G=\' + Gstr, (lx + imgX) / 2, ay + 60);\n' ' }\n' ' }\n' '\n' ) new_content = content[:idx] + NEW_METHODS_1 + content[idx:] content = new_content # ───────────────────────────────────────────────────────────────────────────── # PATCH 2: Add R slider + parabolic/spherical toggle to MirrorSim constructor # ───────────────────────────────────────────────────────────────────────────── # Find MirrorSim constructor and add _R and _parabolic fields OLD_MIRROR_CTOR = ' this._photonPaths = [];\n\n this._prevType = \'concave\';\n this._transT = 1.0;\n this._transRaf = null;\n\n this._drag = null;\n this._hoverX = -999;\n this._hoverY = -999;\n\n this.onUpdate = null;\n this.onAnimate = null;\n\n this._bindEvents();\n new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);\n }' NEW_MIRROR_CTOR = ( ' this._photonPaths = [];\n\n' ' this._prevType = \'concave\';\n' ' this._transT = 1.0;\n' ' this._transRaf = null;\n\n' ' this._drag = null;\n' ' this._hoverX = -999;\n' ' this._hoverY = -999;\n\n' ' this.onUpdate = null;\n' ' this.onAnimate = null;\n\n' ' /* Feature 2: R slider + spherical aberration toggle */\n' ' this._R = 240; // radius of curvature (positive=concave, negative=convex)\n' ' this._useR = false; // true = R-slider mode; false = classic type+f mode\n' ' this._parabolic = false; // false = spherical mirror; true = perfect parabolic\n' '\n' ' this._bindEvents();\n' ' new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);\n' ' }' ) if OLD_MIRROR_CTOR not in content: print('ERROR: MirrorSim ctor block not found'); sys.exit(1) content = content.replace(OLD_MIRROR_CTOR, NEW_MIRROR_CTOR, 1) # ───────────────────────────────────────────────────────────────────────────── # PATCH 3: Add setMirrorR, setMirrorParabolic, _drawAberrationFan # before MirrorSim._bindEvents # ───────────────────────────────────────────────────────────────────────────── # Find the _bindEvents in MirrorSim (it's further down, after chk method) MIRROR_BIND_MARKER = ' _bindEvents() {\n const cv = this.canvas;\n const getPos = e => {' idx2 = content.find(MIRROR_BIND_MARKER) if idx2 == -1: print('ERROR: chk marker not found'); sys.exit(1) NEW_METHODS_2 = ( ' /* === Feature 2: R-slider mode for MirrorSim === */\n' ' setMirrorR(R) {\n' ' this._useR = true;\n' ' this._R = +R;\n' ' // Derive type and f from R\n' ' const absR = Math.abs(this._R);\n' ' if (absR < 5) { this.type = \'flat\'; }\n' ' else if (this._R > 0) { this.type = \'concave\'; this.f = absR / 2; }\n' ' else { this.type = \'convex\'; this.f = absR / 2; }\n' ' this.draw(); this._emit();\n' ' }\n' '\n' ' setMirrorParabolic(on) {\n' ' this._parabolic = !!on;\n' ' this.draw();\n' ' }\n' '\n' ' /* Draw 5 parallel rays showing spherical vs parabolic aberration */\n' ' _drawAberrationFan(ctx, mx, ay, f) {\n' ' if (!isFinite(f) || Math.abs(f) < 5) return;\n' ' const mH = Math.min(this.H * 0.38, 140);\n' ' const heights = [-0.85, -0.45, 0, 0.45, 0.85];\n' ' const COLORS = [\'#FF6B6B\', \'#FFD166\', \'#7BF5A4\', \'#06D6E0\', \'#B8A4FF\'];\n' ' ctx.save(); ctx.lineWidth = 1.4;\n' ' heights.forEach((fr, i) => {\n' ' const rayH = fr * mH;\n' ' // For parabolic mirror: all parallel rays focus exactly at f\n' ' // For spherical: marginal rays (fr != 0) focus closer by h^2/(2R) approx\n' ' const fEff = this._parabolic ? f : f - (rayH * rayH) / (2 * Math.abs(f) * 2);\n' ' const startX = mx - this.d - 40;\n' ' const hitY = ay - rayH; // hits mirror at height rayH\n' ' // Incident ray: horizontal from left to mirror\n' ' ctx.strokeStyle = COLORS[i]; ctx.globalAlpha = 0.75;\n' ' ctx.setLineDash([]);\n' ' ctx.beginPath(); ctx.moveTo(startX, ay - rayH); ctx.lineTo(mx, hitY); ctx.stroke();\n' ' // Reflected ray: goes toward focal point fEff\n' ' const focX = mx - fEff;\n' ' if (focX > 0 && focX < this.W) {\n' ' ctx.beginPath(); ctx.moveTo(mx, hitY); ctx.lineTo(focX, ay);\n' ' // extend a bit past focus\n' ' const dx = focX - mx, dy = ay - hitY, len = Math.hypot(dx, dy);\n' ' if (len > 1) ctx.lineTo(focX + dx/len*50, ay + dy/len*50);\n' ' ctx.stroke();\n' ' }\n' ' });\n' ' ctx.globalAlpha = 1;\n' ' // label\n' ' const label = this._parabolic ? \'Параболическое (идеальный фокус)\' : \'Сферическое (аберрация)\';\n' ' const col = this._parabolic ? \'#7BF5A4\' : \'#FF6B6B\';\n' ' const bx = 12, by = this.H - 36;\n' ' ctx.fillStyle = \'rgba(13,13,26,0.85)\';\n' ' ctx.beginPath(); ctx.roundRect(bx, by, 250, 24, 6); ctx.fill();\n' ' ctx.font = \'bold 11px Manrope, system-ui, sans-serif\';\n' ' ctx.textAlign = \'left\'; ctx.textBaseline = \'middle\';\n' ' ctx.fillStyle = col;\n' ' ctx.fillText(label, bx + 8, by + 12);\n' ' ctx.restore();\n' ' }\n' '\n' ) new_content2 = content[:idx2] + NEW_METHODS_2 + content[idx2:] content = new_content2 with open(src, 'w', encoding='utf-8') as f: f.write(content) print('OK lines:', content.count('\n'))