Files
Learn_System/backend/scripts/patch_optics.py
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

205 lines
11 KiB
Python

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