diff --git a/frontend/js/labs/opticsbench.js b/frontend/js/labs/opticsbench.js
index d65d92c..e97cb92 100644
--- a/frontend/js/labs/opticsbench.js
+++ b/frontend/js/labs/opticsbench.js
@@ -2616,7 +2616,11 @@ class BenchSim {
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 });
+ // white light → one sub-ray per spectral sample (they coincide until a prism disperses them)
+ const wls = window._obWhiteLight ? OB_SPECTRAL.map(s => s.nm) : [window._obWavelength || 540];
+ 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;
for (let i = 0; i < n; i++) {
@@ -2710,14 +2714,16 @@ class BenchSim {
return true;
}
- // Placeholder until Phase 2 (Snell + dispersion); acts as a weak deflector.
+ // Thin-prism deviation δ = (n−1)·A toward the base, with chromatic dispersion
+ // via n(λ) — different wavelengths bend differently → spectrum fan.
_prismInteract(ray, el, yRel) {
if (Math.abs(yRel) > el.size) return false;
+ const n = (typeof _nAtWavelength === 'function') ? _nAtWavelength(el.n, ray.wl) : el.n;
+ const A = el.apex * Math.PI / 180;
+ const dev = (n - 1) * A; // radians, toward the base (+y)
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;
+ const ang = Math.atan2(ray.dy, ray.dx) + sgn * dev;
+ ray.dx = Math.cos(ang); ray.dy = Math.sin(ang);
return true;
}
@@ -2730,13 +2736,14 @@ class BenchSim {
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
+ // trace rays — colour each by its wavelength (so dispersion shows as a fan)
const rays = this._emitRays();
- const rayColor = (typeof _obRayColor === 'function') ? _obRayColor(window._obWavelength || 540) : '#06D6E0';
+ const white = !!window._obWhiteLight;
ctx.lineWidth = 1.1;
for (const ray of rays) {
this._traceRay(ray);
- ctx.strokeStyle = rayColor; ctx.globalAlpha = 0.8;
+ ctx.strokeStyle = (typeof wavelengthToRGB === 'function') ? wavelengthToRGB(ray.wl) : '#06D6E0';
+ ctx.globalAlpha = white ? 0.5 : 0.82;
ctx.beginPath();
ray.pts.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
ctx.stroke();
@@ -3801,7 +3808,7 @@ function _obRedraw() {
if (_obMode === 'mirror' && mirrorSim) { mirrorSim.draw(); }
if (_obMode === 'refraction' && refrSim) { refrSim.draw(); }
if (_obMode === 'prism' && prismSim) { prismSim.draw(); }
- if (_obMode === 'freebuild' && freeSim) { freeSim.draw(); }
+ if (_obMode === 'freebuild' && benchSim) { benchSim.draw(); }
if (_obMode === 'waves' && diffrSim) { diffrSim.draw(); diffrSim._updateHUD(); }
if (_obMode === 'interf' && ifSim) { ifSim.draw(); }
_obDrawSpectrometer();
diff --git a/frontend/lab.html b/frontend/lab.html
index 0056557..0249e02 100644
--- a/frontend/lab.html
+++ b/frontend/lab.html
@@ -4841,7 +4841,7 @@
-
+
diff --git a/plans/OPTICS_CONSTRUCTOR.md b/plans/OPTICS_CONSTRUCTOR.md
index 3be3485..b45fef2 100644
--- a/plans/OPTICS_CONSTRUCTOR.md
+++ b/plans/OPTICS_CONSTRUCTOR.md
@@ -30,9 +30,9 @@
- [x] 1.5 Встроено в режим `freebuild` (вкладка «Конструктор»), пресеты систем (микроскоп/телескоп/проектор/зеркальная), сохранение состояния в снимок.
- Призма пока — грубый дефлектор-placeholder (настоящий Снеллиус в 2.2).
-### Фаза 2 — Сферические зеркала + призма + дисперсия — [ ]
-- [ ] 2.1 Вогнутое/выпуклое зеркало (кик f=R/2, разворот хода, лимит отражений).
-- [ ] 2.2 Призма: Снеллиус на 2 гранях, дисперсия по длине волны, белый свет.
+### Фаза 2 — Сферические зеркала + призма + дисперсия — [x]
+- [x] 2.1 Вогнутое/выпуклое зеркало — сделано ещё в Фазе 1 (кик f=R/2, разворот хода, лимит отражений).
+- [x] 2.2 Призма: тонкопризменное отклонение δ=(n−1)·A к основанию + хроматическая дисперсия n(λ). Белый свет — пучки по `OB_SPECTRAL`, каждый луч красится по λ (`wavelengthToRGB`); до призмы совпадают, после — расходятся в спектр. Управление через общий λ-бар скамьи. Проверено численно (фиолетовый отклоняется сильнее красного).
### Фаза 3 — Сохранение состояния + полировка — [ ]
- [ ] 3.1 Расширить `_obGetState/_obApplyState` на конструктор (снимок/embed).