diff --git a/frontend/js/labs/opticsbench.js b/frontend/js/labs/opticsbench.js index 3dda0ce..21a0b4e 100644 --- a/frontend/js/labs/opticsbench.js +++ b/frontend/js/labs/opticsbench.js @@ -2544,8 +2544,8 @@ class BenchSim { 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 }; + // source: object arrow by default. `ang` (deg) aims point/single/laser/parallel. + this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9, ang: 0 }; // elements along the bench, positioned by x-fraction; centred on the axis this.elements = [ this._mk('lens', { xf: 0.40, f: 130, ap: 95 }), @@ -2565,6 +2565,8 @@ class BenchSim { 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 }; + if (type === 'interface') return { ...base, n1: p.n1 != null ? p.n1 : 1, n2: p.n2 != null ? p.n2 : 1.5, ap: p.ap || 110 }; + if (type === 'slab') return { ...base, n: p.n != null ? p.n : 1.5, t: p.t != null ? p.t : 60, ap: p.ap || 95 }; return base; } @@ -2621,15 +2623,26 @@ class BenchSim { const push = (x, y, ang) => { for (const wl of wls) rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), wl, pts: [{ x, y }], alive: true, bounces: 0 }); }; - if (this.source.kind === 'parallel') { - const n = 9, hh = 90; + const aim = (this.source.ang || 0) * Math.PI / 180; + if (this.source.kind === 'single') { + push(sx, ay, aim); // one aimable ray + } else if (this.source.kind === 'laser') { + const n = 3, hh = 7; // narrow collimated beam + const px = -Math.sin(aim), py = Math.cos(aim); // perpendicular to aim for (let i = 0; i < n; i++) { - const y = ay - hh + (2 * hh) * (i / (n - 1)); - push(sx, y, 0); + const o = (i - (n - 1) / 2) * hh; + push(sx + px * o, ay + py * o, aim); + } + } else if (this.source.kind === 'parallel') { + const n = 9, hh = 90; + const px = -Math.sin(aim), py = Math.cos(aim); + for (let i = 0; i < n; i++) { + const o = -hh + (2 * hh) * (i / (n - 1)); + push(sx + px * o, ay + py * o, aim); } } 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))); + for (let i = 0; i < n; i++) push(sx, ay, aim - 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 => { @@ -2682,14 +2695,38 @@ class BenchSim { _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 + if (Math.abs(yRel) > el.ap) { ray.alive = false; return true; } // mount blocks outside aperture 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 (el.type === 'interface') { if (Math.abs(yRel) > el.ap) return false; + // Snell at a vertical plane (normal = x). Tangential (y) component scales by n_i/n_t. + const goingRight = ray.dx > 0; + const ni = goingRight ? el.n1 : el.n2; + const nt = goingRight ? el.n2 : el.n1; + const dyT = (ni / nt) * ray.dy; // sinθ_t (tangential preserved) + if (Math.abs(dyT) >= 1) { ray.dx = -ray.dx; ray.bounces++; return true; } // total internal reflection + const sgn = Math.sign(ray.dx) || 1; + ray.dy = dyT; ray.dx = sgn * Math.sqrt(Math.max(0, 1 - dyT * dyT)); + return true; + } + if (el.type === 'slab') { + if (Math.abs(yRel) > el.ap) return false; + // parallel plate: ray exits parallel but laterally shifted (refract in, travel t, refract out) + const sinI = ray.dy; // |d|=1 → y-comp is sinθ from axis + const sinT = sinI / el.n; + const tanT = sinT / Math.sqrt(Math.max(1e-6, 1 - sinT * sinT)); + const sgn = Math.sign(ray.dx) || 1; + ray.pts.push({ x: ray.x, y: ray.y }); // entry on the front face + ray.x += sgn * el.t; // emerge on the far face + ray.y += tanT * el.t; // inside-the-glass vertical travel + return true; // direction unchanged (parallel faces); tracer pushes exit + } + if (el.type === 'mirror') { + if (Math.abs(yRel) > el.ap) { ray.alive = false; return true; } ray.dx = -ray.dx; // reflect about the vertical plane ray.bounces++; if (el.kind !== 'plane') { @@ -2784,15 +2821,22 @@ class BenchSim { _drawSource(ctx, ay) { const sx = this.source.xf * this.W; + const aim = (this.source.ang || 0) * Math.PI / 180; 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 isLaser = this.source.kind === 'laser', isSingle = this.source.kind === 'single'; + ctx.fillStyle = (this.source.kind === 'point' || isSingle) ? '#FFD166' : '#9B5DE5'; + ctx.beginPath(); ctx.arc(sx, ay, isLaser ? 4 : 5, 0, Math.PI * 2); ctx.fill(); + if (this.source.kind === 'parallel' || isLaser) { + const px = -Math.sin(aim), py = Math.cos(aim), hh = isLaser ? 10 : 90; + ctx.strokeStyle = isLaser ? '#FF5B5B' : '#9B5DE5'; ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(sx - px * hh, ay - py * hh); ctx.lineTo(sx + px * hh, ay + py * hh); ctx.stroke(); + } + // aim arrow for single / laser / point + if (isSingle || isLaser || this.source.kind === 'point') { + this._arrow(ctx, sx, ay, sx + 22 * Math.cos(aim), ay + 22 * Math.sin(aim), isLaser ? '#FF5B5B' : '#FFD166'); } } const sel = this.selectedId === '__src'; @@ -2816,7 +2860,26 @@ class BenchSim { [[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(); }); + // focal markers F and 2F on both sides of the lens + if (conv) { + ctx.fillStyle = 'rgba(6,214,224,0.6)'; + [el.f, -el.f, 2 * el.f, -2 * el.f].forEach((d, i) => { + const fx = x + d; if (fx < 6 || fx > this.W - 6) return; + ctx.beginPath(); ctx.arc(fx, ay, 3, 0, Math.PI * 2); ctx.fill(); + this._elLabel(ctx, fx, ay - 16, i < 2 ? 'F' : '2F'); + }); + } this._elLabel(ctx, x, ay + el.ap + 14, (conv ? 'линза +' : 'линза −') + Math.abs(el.f).toFixed(0)); + } else if (el.type === 'interface') { + ctx.fillStyle = 'rgba(96,165,250,0.10)'; ctx.fillRect(x, ay - el.ap, this.W - x, 2 * el.ap); + ctx.strokeStyle = sel ? '#fff' : '#60a5fa'; + ctx.beginPath(); ctx.moveTo(x, ay - el.ap); ctx.lineTo(x, ay + el.ap); ctx.stroke(); + this._elLabel(ctx, x, ay + el.ap + 14, 'граница ' + el.n1.toFixed(2) + ' | ' + el.n2.toFixed(2)); + } else if (el.type === 'slab') { + ctx.fillStyle = 'rgba(123,245,164,0.10)'; ctx.fillRect(x, ay - el.ap, el.t, 2 * el.ap); + ctx.strokeStyle = sel ? '#fff' : '#7BF5A4'; + ctx.strokeRect(x, ay - el.ap, el.t, 2 * el.ap); + this._elLabel(ctx, x + el.t / 2, ay + el.ap + 14, 'пластина n=' + el.n.toFixed(2)); } else if (el.type === 'mirror') { ctx.strokeStyle = sel ? '#fff' : '#A8E063'; ctx.beginPath(); @@ -4350,65 +4413,72 @@ function _freeUpdateUI(chain) { /* ── 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 'Призма'; + 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 'Призма'; + if (e.type === 'interface') return 'Граница сред'; + if (e.type === 'slab') return 'Пластина'; return e.type; } -function _benchRow(label, html) { - return '
'; +// Labelled slider with a live numeric value (no panel rebuild → drag stays smooth). +function _benchCtl(label, id, key, min, max, step, val, isSource) { + const vid = 'bv_' + (isSource ? 's' : id) + '_' + key; + const call = isSource ? "benchSourceParam('" + key + "',this.value)" : "benchUpdate(" + id + ",'" + key + "',this.value)"; + return ''; } -function _benchSlider(id, key, min, max, step, val) { - return ''; +function _benchBtnRow(opts, isActive, onClick) { + return '