diff --git a/frontend/js/labs/opticsbench.js b/frontend/js/labs/opticsbench.js index 8d57a76..d65d92c 100644 --- a/frontend/js/labs/opticsbench.js +++ b/frontend/js/labs/opticsbench.js @@ -2532,6 +2532,365 @@ class FreeBuildSim { } } +/* ───────────────────────────────────────────────────────────── + 4a-BIS. OPTICAL BENCH CONSTRUCTOR — general 2D ray tracer + Mixed elements (lens, mirror, aperture, screen, prism) + sources. +───────────────────────────────────────────────────────────────*/ +class BenchSim { + constructor(canvas) { + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + this.W = 0; this.H = 0; + this.onUpdate = null; + this._drag = null; + this._nextId = 1; + // source: object arrow by default + this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9 }; + // elements along the bench, positioned by x-fraction; centred on the axis + this.elements = [ + this._mk('lens', { xf: 0.40, f: 130, ap: 95 }), + this._mk('screen', { xf: 0.86 }), + ]; + this.selectedId = null; + this._bindEvents(); + this._ro = new ResizeObserver(() => { this.fit(); this.draw(); }); + this._ro.observe(canvas.parentElement || canvas); + } + + _mk(type, p) { + const id = this._nextId++; + const base = { id, type, xf: p.xf != null ? p.xf : 0.5 }; + if (type === 'lens') return { ...base, f: p.f != null ? p.f : 130, ap: p.ap || 95 }; + if (type === 'mirror') return { ...base, kind: p.kind || 'concave', R: p.R != null ? p.R : 320, ap: p.ap || 95 }; + if (type === 'aperture') return { ...base, gap: p.gap != null ? p.gap : 40 }; + if (type === 'screen') return { ...base }; + if (type === 'prism') return { ...base, apex: p.apex != null ? p.apex : 50, n: p.n != null ? p.n : 1.52, size: p.size || 90 }; + return base; + } + + fit() { + const dpr = window.devicePixelRatio || 1; + const w = this.canvas.offsetWidth || 600; + const h = this.canvas.offsetHeight || 400; + this.canvas.width = w * dpr; + this.canvas.height = h * dpr; + this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + this.W = w; this.H = h; + } + + /* ── element API (used by the inspector) ── */ + addElement(type) { + const xf = Math.min(0.92, (this.elements.length ? Math.max(...this.elements.map(e => e.xf)) : 0.4) + 0.14); + const el = this._mk(type, { xf }); + this.elements.push(el); + this.selectedId = el.id; + this._changed(); + return el; + } + removeElement(id) { + this.elements = this.elements.filter(e => e.id !== id); + if (this.selectedId === id) this.selectedId = null; + this._changed(); + } + selectElement(id) { this.selectedId = id; this._changed(); } + updateElement(id, key, val) { + const el = this.elements.find(e => e.id === id); + if (!el) return; + el[key] = (key === 'kind') ? val : +val; + this._redraw(); // canvas only — never rebuild the inspector mid-slider-drag + } + setSource(key, val) { + this.source[key] = (key === 'kind') ? val : +val; + this._redraw(); + } + getSelected() { return this.elements.find(e => e.id === this.selectedId) || null; } + _redraw() { this.draw(); } + _changed() { this.draw(); if (this.onUpdate) this.onUpdate(); } // draw + rebuild inspector + + /* ── geometry helpers ── */ + _ex(el) { return el.xf * this.W; } + _ay() { return this.H / 2; } + + /* Emit the initial rays from the source. */ + _emitRays() { + const ay = this._ay(); + const sx = this.source.xf * this.W; + const rays = []; + const push = (x, y, ang) => rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), pts: [{ x, y }], alive: true, bounces: 0 }); + if (this.source.kind === 'parallel') { + const n = 9, hh = 90; + for (let i = 0; i < n; i++) { + const y = ay - hh + (2 * hh) * (i / (n - 1)); + push(sx, y, 0); + } + } else if (this.source.kind === 'point') { + const n = this.source.rays, A = this.source.spread; + for (let i = 0; i < n; i++) push(sx, ay, -A + 2 * A * (i / (n - 1))); + } else { // object arrow: fan from tip and base + const n = this.source.rays, A = this.source.spread; + [ay - this.source.h, ay].forEach(y0 => { + for (let i = 0; i < n; i++) push(sx, y0, -A + 2 * A * (i / (n - 1))); + }); + } + return rays; + } + + /* Trace one ray through the system, filling ray.pts. */ + _traceRay(ray) { + const eps = 0.5, maxSteps = 40; + const elems = this.elements; + for (let step = 0; step < maxSteps && ray.alive; step++) { + // find nearest element plane ahead + let best = null; + for (const el of elems) { + const ex = this._ex(el); + if (Math.abs(ray.dx) < 1e-6) continue; + const t = (ex - ray.x) / ray.dx; + if (t > eps && (!best || t < best.t)) best = { t, el, ex }; + } + // boundary intersection + const tBound = this._boundT(ray); + if (!best || tBound < best.t) { + const hx = ray.x + ray.dx * tBound, hy = ray.y + ray.dy * tBound; + ray.pts.push({ x: hx, y: hy }); ray.alive = false; break; + } + // advance to element + const hx = ray.x + ray.dx * best.t, hy = ray.y + ray.dy * best.t; + ray.x = hx; ray.y = hy; + const interacted = this._interact(ray, best.el, hy - this._ay()); + ray.pts.push({ x: ray.x, y: ray.y }); + if (!interacted) { ray.x += ray.dx * eps; ray.y += ray.dy * eps; } // missed → step past + if (ray.bounces > 16) ray.alive = false; + } + return ray; + } + + _boundT(ray) { + const ts = []; + if (ray.dx > 1e-9) ts.push((this.W - ray.x) / ray.dx); + else if (ray.dx < -1e-9) ts.push((0 - ray.x) / ray.dx); + if (ray.dy > 1e-9) ts.push((this.H - ray.y) / ray.dy); + else if (ray.dy < -1e-9) ts.push((0 - ray.y) / ray.dy); + return ts.length ? Math.min(...ts.filter(t => t > 0)) : 1e6; + } + + /* Apply an element. Returns true if it interacted (false = ray missed it). */ + _interact(ray, el, yRel) { + const norm = (x, y) => { const l = Math.hypot(x, y) || 1; ray.dx = x / l; ray.dy = y / l; }; + if (el.type === 'lens') { + if (Math.abs(yRel) > el.ap) return false; // outside aperture → pass + const sgn = Math.sign(ray.dx) || 1; + const w = ray.dy / Math.abs(ray.dx); + norm(sgn, w - yRel / el.f); + return true; + } + if (el.type === 'mirror') { + if (Math.abs(yRel) > el.ap) return false; + ray.dx = -ray.dx; // reflect about the vertical plane + ray.bounces++; + if (el.kind !== 'plane') { + const fM = (el.kind === 'concave' ? 1 : -1) * el.R / 2; + const sgn = Math.sign(ray.dx) || 1; + const w = ray.dy / Math.abs(ray.dx); + norm(sgn, w - yRel / fM); + } + return true; + } + if (el.type === 'aperture') { + if (Math.abs(yRel) > el.gap) ray.alive = false; // blocked by the stop + return true; + } + if (el.type === 'screen') { + ray.hitY = ray.y; ray.alive = false; // absorbed, hit recorded + return true; + } + if (el.type === 'prism') { + return this._prismInteract(ray, el, yRel); + } + return true; + } + + // Placeholder until Phase 2 (Snell + dispersion); acts as a weak deflector. + _prismInteract(ray, el, yRel) { + if (Math.abs(yRel) > el.size) return false; + const sgn = Math.sign(ray.dx) || 1; + const dev = (el.apex / 60) * (el.n - 1) * 0.5; // crude downward deviation toward the base + const x = sgn, y = ray.dy / Math.abs(ray.dx) + dev; + const l = Math.hypot(x, y) || 1; + ray.dx = x / l; ray.dy = y / l; + return true; + } + + draw() { + const { ctx, W, H } = this; + if (!W || !H) return; + const ay = this._ay(); + ctx.fillStyle = '#0D0D1A'; ctx.fillRect(0, 0, W, H); + // optical axis + ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; ctx.setLineDash([6, 4]); + ctx.beginPath(); ctx.moveTo(0, ay); ctx.lineTo(W, ay); ctx.stroke(); ctx.setLineDash([]); + + // trace rays + const rays = this._emitRays(); + const rayColor = (typeof _obRayColor === 'function') ? _obRayColor(window._obWavelength || 540) : '#06D6E0'; + ctx.lineWidth = 1.1; + for (const ray of rays) { + this._traceRay(ray); + ctx.strokeStyle = rayColor; ctx.globalAlpha = 0.8; + ctx.beginPath(); + ray.pts.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)); + ctx.stroke(); + } + ctx.globalAlpha = 1; + + // source + elements + this._drawSource(ctx, ay); + for (const el of this.elements) this._drawElement(ctx, el, ay); + + if (typeof _drawOBFXLayer === 'function') { + _drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: ay - (this.source.h || 0) }); + } + } + + _drawSource(ctx, ay) { + const sx = this.source.xf * this.W; + ctx.save(); + if (this.source.kind === 'object') { + this._arrow(ctx, sx, ay, sx, ay - this.source.h, '#9B5DE5'); + } else { + ctx.fillStyle = this.source.kind === 'point' ? '#FFD166' : '#9B5DE5'; + ctx.beginPath(); ctx.arc(sx, ay, 5, 0, Math.PI * 2); ctx.fill(); + if (this.source.kind === 'parallel') { + ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(sx, ay - 90); ctx.lineTo(sx, ay + 90); ctx.stroke(); + } + } + const sel = this.selectedId === '__src'; + ctx.fillStyle = sel ? '#fff' : 'rgba(155,93,229,0.9)'; + ctx.font = '10px Manrope, system-ui, sans-serif'; ctx.textAlign = 'center'; + ctx.fillText('источник', sx, ay + 16); + ctx.restore(); + } + + _drawElement(ctx, el, ay) { + const x = this._ex(el); + const sel = el.id === this.selectedId; + ctx.save(); + ctx.lineWidth = sel ? 3 : 2; + if (el.type === 'lens') { + const conv = el.f >= 0; + ctx.strokeStyle = sel ? '#fff' : '#06D6E0'; + ctx.beginPath(); ctx.moveTo(x, ay - el.ap); ctx.lineTo(x, ay + el.ap); ctx.stroke(); + // arrow tips to denote converging/diverging + const tip = conv ? 7 : -7; + [[ay - el.ap, 1], [ay + el.ap, -1]].forEach(([yy, s]) => { + ctx.beginPath(); ctx.moveTo(x, yy); ctx.lineTo(x - tip, yy + s * 7); ctx.moveTo(x, yy); ctx.lineTo(x + tip, yy + s * 7); ctx.stroke(); + }); + this._elLabel(ctx, x, ay + el.ap + 14, (conv ? 'линза +' : 'линза −') + Math.abs(el.f).toFixed(0)); + } else if (el.type === 'mirror') { + ctx.strokeStyle = sel ? '#fff' : '#A8E063'; + ctx.beginPath(); + if (el.kind === 'plane') { ctx.moveTo(x, ay - el.ap); ctx.lineTo(x, ay + el.ap); } + else { + const bow = (el.kind === 'concave' ? -1 : 1) * 14; + ctx.moveTo(x, ay - el.ap); + ctx.quadraticCurveTo(x + bow, ay, x, ay + el.ap); + } + ctx.stroke(); + // hatch backside + ctx.strokeStyle = 'rgba(168,224,99,0.4)'; ctx.lineWidth = 1; + for (let yy = -el.ap; yy < el.ap; yy += 12) { ctx.beginPath(); ctx.moveTo(x, ay + yy); ctx.lineTo(x + 6, ay + yy + 6); ctx.stroke(); } + this._elLabel(ctx, x, ay + el.ap + 14, 'зеркало ' + ({ plane: 'плоск', concave: 'вогн', convex: 'выпукл' }[el.kind])); + } else if (el.type === 'aperture') { + ctx.strokeStyle = sel ? '#fff' : '#EF476F'; ctx.lineWidth = sel ? 5 : 4; + ctx.beginPath(); ctx.moveTo(x, ay - 110); ctx.lineTo(x, ay - el.gap); ctx.moveTo(x, ay + el.gap); ctx.lineTo(x, ay + 110); ctx.stroke(); + this._elLabel(ctx, x, ay + 124, 'диафрагма'); + } else if (el.type === 'screen') { + ctx.strokeStyle = sel ? '#fff' : 'rgba(255,255,255,0.7)'; ctx.lineWidth = sel ? 5 : 4; + ctx.beginPath(); ctx.moveTo(x, ay - 110); ctx.lineTo(x, ay + 110); ctx.stroke(); + this._elLabel(ctx, x, ay + 124, 'экран'); + } else if (el.type === 'prism') { + ctx.strokeStyle = sel ? '#fff' : '#FFD166'; ctx.fillStyle = 'rgba(255,209,102,0.12)'; + ctx.beginPath(); ctx.moveTo(x, ay - el.size); ctx.lineTo(x + el.size * 0.7, ay + el.size); ctx.lineTo(x - el.size * 0.7, ay + el.size); ctx.closePath(); + ctx.fill(); ctx.stroke(); + this._elLabel(ctx, x, ay + el.size + 14, 'призма'); + } + ctx.restore(); + } + + _elLabel(ctx, x, y, text) { + ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '10px Manrope, system-ui, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(text, x, y); + } + + _arrow(ctx, x0, y0, x1, y1, color) { + ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 2.5; + ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); + const a = Math.atan2(y1 - y0, x1 - x0); + ctx.beginPath(); ctx.moveTo(x1, y1); + ctx.lineTo(x1 - 9 * Math.cos(a - 0.4), y1 - 9 * Math.sin(a - 0.4)); + ctx.lineTo(x1 - 9 * Math.cos(a + 0.4), y1 - 9 * Math.sin(a + 0.4)); + ctx.closePath(); ctx.fill(); + } + + /* ── interaction: drag + select ── */ + _bindEvents() { + const cv = this.canvas; + this._listeners = []; + const on = (t, ty, fn, o) => { t.addEventListener(ty, fn, o); this._listeners.push([t, ty, fn, o]); }; + const pos = (e) => { + const r = cv.getBoundingClientRect(); + const t = e.touches ? e.touches[0] : e; + return { mx: (t.clientX - r.left) * (this.W / r.width), my: (t.clientY - r.top) * (this.H / r.height) }; + }; + const hit = (mx, my) => { + const ay = this._ay(); + for (const el of this.elements) { + if (Math.abs(mx - this._ex(el)) < 14 && Math.abs(my - ay) < 120) return { kind: 'el', id: el.id }; + } + const sx = this.source.xf * this.W; + if (Math.abs(mx - sx) < 16 && Math.abs(my - ay) < 120) return { kind: 'src' }; + return null; + }; + on(cv, 'pointerdown', e => { + const { mx, my } = pos(e); + const h = hit(mx, my); + this._drag = h; + if (h) { + this.selectedId = h.kind === 'src' ? '__src' : h.id; + try { cv.setPointerCapture(e.pointerId); } catch (_) {} + this._changed(); + } else { this.selectedId = null; this._changed(); } + }); + on(cv, 'pointermove', e => { + if (!this._drag) { const { mx, my } = pos(e); cv.style.cursor = hit(mx, my) ? 'grab' : 'default'; return; } + const { mx } = pos(e); + const xf = Math.max(0.02, Math.min(0.98, mx / this.W)); + if (this._drag.kind === 'src') this.source.xf = xf; + else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; } + this._redraw(); // position drag → redraw canvas, keep inspector intact + }); + on(cv, 'pointerup', e => { this._drag = null; try { cv.releasePointerCapture(e.pointerId); } catch (_) {} }); + } + + dispose() { + if (this._ro) { this._ro.disconnect(); this._ro = null; } + if (this._listeners) { for (const [t, ty, fn, o] of this._listeners) t.removeEventListener(ty, fn, o); this._listeners = []; } + } + + /* ── state (for snapshot / embed) ── */ + getState() { return { source: { ...this.source }, elements: this.elements.map(e => ({ ...e })) }; } + setState(st) { + if (!st) return; + if (st.source) this.source = { ...this.source, ...st.source }; + if (Array.isArray(st.elements)) { + this.elements = st.elements.map(e => ({ ...e })); + this._nextId = this.elements.reduce((m, e) => Math.max(m, e.id || 0), 0) + 1; + } + this._changed(); + } +} + /* ───────────────────────────────────────────────────────────── 4b. PRISM ENGINE ───────────────────────────────────────────────────────────────*/ @@ -3192,7 +3551,8 @@ var lensSim = null; var mirrorSim = null; var refrSim = null; var prismSim = null; -var freeSim = null; /* multi-lens free-build (Agent OB-A3) */ +var freeSim = null; /* multi-lens free-build (legacy, superseded by benchSim) */ +var benchSim = null; /* optical bench constructor (general ray tracer) */ var ifSim = null; /* interference/polarization (Agent C) */ var _obMode = 'lens'; // current active mode within opticsbench @@ -3222,6 +3582,7 @@ function _obGetState() { if (_obMode === 'lens') return { mode: 'lens', ...(lensSim ? lensSim.getParams() : {}) }; if (_obMode === 'mirror') return { mode: 'mirror', ...(mirrorSim ? mirrorSim.getParams() : {}) }; if (_obMode === 'refraction') return { mode: 'refraction', ...(refrSim ? refrSim.getParams() : {}) }; + if (_obMode === 'freebuild') return { mode: 'freebuild', bench: benchSim ? benchSim.getState() : null }; if (_obMode === 'waves') return { mode: 'waves' }; return { mode: _obMode }; } @@ -3234,6 +3595,7 @@ function _obApplyState(st) { if (m === 'lens' && lensSim) lensSim.setParams(params); if (m === 'mirror' && mirrorSim) mirrorSim.setParams(params); if (m === 'refraction' && refrSim) refrSim.setParams(params); + if (m === 'freebuild' && benchSim && st.bench) { benchSim.setState(st.bench); _benchUpdateUI(); } } /* Switch between modes — mirrors emSwitchMode pattern */ @@ -3317,15 +3679,15 @@ function obSwitchMode(mode, silent) { } if (prismSim) { prismSim.fit(); prismSim.draw(); } _obDrawSpectrometer(); - } else if (mode === 'freebuild') { /* Agent OB-A3 — multi-lens free build */ - if (!freeSim) { + } else if (mode === 'freebuild') { /* Optical bench constructor (BenchSim) */ + if (!benchSim) { const cv = document.getElementById('ob-free-canvas'); if (cv) { - freeSim = new FreeBuildSim(cv); - freeSim.onUpdate = _freeUpdateUI; + benchSim = new BenchSim(cv); + benchSim.onUpdate = _benchUpdateUI; } } - if (freeSim) { freeSim.fit(); freeSim.draw(); _freeUpdateUI(freeSim._computeChain()); } + if (benchSim) { benchSim.fit(); benchSim.draw(); _benchUpdateUI(); } } else if (mode === 'waves') { /* Agent B1 — diffraction & interference */ if (!diffrSim) { const cv = document.getElementById('ob-waves-canvas'); @@ -3952,6 +4314,111 @@ function _freeUpdateUI(chain) { if (el2) el2.textContent = chain.sysFocal !== null ? chain.sysFocal.toFixed(0) : '—'; } +/* ── Optical bench constructor (BenchSim) UI ── */ +function _benchElName(e) { + if (e.type === 'lens') return (e.f >= 0 ? 'Линза +' : 'Линза −') + Math.abs(e.f).toFixed(0); + if (e.type === 'mirror') return 'Зеркало ' + ({ plane: 'плоск', concave: 'вогн', convex: 'выпукл' }[e.kind] || ''); + if (e.type === 'aperture') return 'Диафрагма'; + if (e.type === 'screen') return 'Экран'; + if (e.type === 'prism') return 'Призма'; + return e.type; +} +function _benchRow(label, html) { + return '
' + html + '
'; +} +function _benchSlider(id, key, min, max, step, val) { + return ''; +} +function _benchPropsHTML() { + if (!benchSim) return ''; + const sel = benchSim.selectedId; + if (sel === '__src') { + const s = benchSim.source; + let h = '
Источник
'; + h += '
' + + ['object:Предмет', 'point:Точка', 'parallel:Параллель'].map(o => { + const [k, lbl] = o.split(':'); + return ''; + }).join('') + '
'; + if (s.kind === 'object') h += _benchRow('Высота', _benchSourceSlider('h', 20, 120, 2, s.h)); + if (s.kind !== 'parallel') h += _benchRow('Раствор', _benchSourceSlider('spread', 0.1, 0.6, 0.02, s.spread)); + return h; + } + const e = benchSim.getSelected(); + if (!e) return '
Выберите элемент или источник (клик по схеме)
'; + let h = '
' + _benchElName(e) + '
'; + if (e.type === 'lens') { + h += _benchRow('f, px', _benchSlider(e.id, 'f', -300, 300, 5, e.f)); + h += _benchRow('Апертура', _benchSlider(e.id, 'ap', 30, 130, 5, e.ap)); + } else if (e.type === 'mirror') { + h += '
' + + ['plane:Плоск', 'concave:Вогн', 'convex:Выпукл'].map(o => { + const [k, lbl] = o.split(':'); + return ''; + }).join('') + '
'; + if (e.kind !== 'plane') h += _benchRow('R, px', _benchSlider(e.id, 'R', 100, 600, 10, e.R)); + h += _benchRow('Апертура', _benchSlider(e.id, 'ap', 30, 130, 5, e.ap)); + } else if (e.type === 'aperture') { + h += _benchRow('Зазор', _benchSlider(e.id, 'gap', 5, 110, 2, e.gap)); + } else if (e.type === 'prism') { + h += _benchRow('Угол', _benchSlider(e.id, 'apex', 20, 70, 1, e.apex)); + h += _benchRow('n', _benchSlider(e.id, 'n', 1.3, 1.9, 0.01, e.n)); + h += _benchRow('Размер', _benchSlider(e.id, 'size', 50, 130, 5, e.size)); + } else if (e.type === 'screen') { + h += '
Экран ловит изображение.
'; + } + h += ''; + return h; +} +function _benchSourceSlider(key, min, max, step, val) { + return ''; +} +function _benchUpdateUI() { + if (!benchSim) return; + const listEl = document.getElementById('bench-list'); + if (listEl) { + listEl.innerHTML = benchSim.elements.map(e => + '' + ).join('') || '
Пусто
'; + } + const propsEl = document.getElementById('bench-props'); + if (propsEl) propsEl.innerHTML = _benchPropsHTML(); +} +function benchAdd(type) { if (benchSim) { benchSim.addElement(type); _benchUpdateUI(); } } +function benchRemove(id) { if (benchSim) { benchSim.removeElement(id); _benchUpdateUI(); } } +function benchSelect(id) { if (benchSim) { benchSim.selectElement(id); _benchUpdateUI(); } } +function benchUpdate(id, k, v) { if (benchSim) benchSim.updateElement(id, k, v); } +function benchSourceKind(k) { if (benchSim) { benchSim.setSource('kind', k); _benchUpdateUI(); } } +function benchSourceParam(k, v){ if (benchSim) benchSim.setSource(k, v); } +function benchClear() { + if (!benchSim) return; + benchSim.elements = []; benchSim.selectedId = null; benchSim._changed(); _benchUpdateUI(); +} +function benchPreset(name) { + if (!benchSim) return; + const P = { + microscope: { source: { kind: 'object', xf: 0.06, h: 40, spread: 0.32, rays: 9 }, + elements: [{ type: 'lens', xf: 0.30, f: 45, ap: 90 }, { type: 'lens', xf: 0.66, f: 90, ap: 95 }, { type: 'screen', xf: 0.92 }] }, + telescope: { source: { kind: 'parallel', xf: 0.05, h: 0, spread: 0.2, rays: 9 }, + elements: [{ type: 'lens', xf: 0.28, f: 200, ap: 95 }, { type: 'lens', xf: 0.74, f: 60, ap: 80 }] }, + projector: { source: { kind: 'object', xf: 0.10, h: 80, spread: 0.34, rays: 9 }, + elements: [{ type: 'lens', xf: 0.40, f: 120, ap: 100 }, { type: 'screen', xf: 0.92 }] }, + folded: { source: { kind: 'object', xf: 0.08, h: 60, spread: 0.3, rays: 9 }, + elements: [{ type: 'lens', xf: 0.34, f: 150, ap: 90 }, { type: 'mirror', xf: 0.82, kind: 'concave', R: 320, ap: 100 }, { type: 'screen', xf: 0.50 }] }, + }; + const p = P[name]; if (!p) return; + let id = 1; + benchSim.source = { ...p.source }; + benchSim.elements = p.elements.map(e => ({ id: id++, ...e })); + benchSim._nextId = id; + benchSim.selectedId = null; + benchSim._changed(); + _benchUpdateUI(); +} + /* ───────────────────────────────────────────────────────────── 6. DIFFRACTION SIM — Волновая оптика (Юнг / Однощелевая / Решётка) ───────────────────────────────────────────────────────────────*/ diff --git a/frontend/lab.html b/frontend/lab.html index 94c3782..0056557 100644 --- a/frontend/lab.html +++ b/frontend/lab.html @@ -2934,7 +2934,7 @@ - + @@ -3186,28 +3186,27 @@ -