From 6a3d1e04d04d8892939511f96ba26125c2f16e92 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 13:28:15 +0300 Subject: [PATCH] =?UTF-8?q?feat(opticsbench):=20=D1=80=D0=B5=D0=B6=D0=B8?= =?UTF-8?q?=D0=BC=20=D0=BB=D1=83=D1=87=D0=B5=D0=B9=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=BC=D0=B5=D1=82=D0=B0=20=E2=80=94=20=D1=85=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B5=D1=80=D0=B8=D1=81=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20vs=20=D0=BF=D1=83=D1=87=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - источник «Предмет»: тумблер «Характ. лучи» (по умолчанию) / «Пучок» - характеристические: 3 луча от вершины (параллельный→F', через центр, через F→параллельно) + осевой от основания — как в учебнике; проверено численно (F'=lensX+f, центр прямо, через F выход параллелен) - пучок: прежний физичный веер + ползунок «Лучей» (густота) и «Раствор» - setSource: rayMode как строковый ключ; bump opticsbench.js?v=9 Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/labs/_pilots.js | 45 --------------------------------- frontend/js/labs/opticsbench.js | 42 ++++++++++++++++++++++++------ frontend/lab.html | 2 +- 3 files changed, 35 insertions(+), 54 deletions(-) delete mode 100644 frontend/js/labs/_pilots.js diff --git a/frontend/js/labs/_pilots.js b/frontend/js/labs/_pilots.js deleted file mode 100644 index e897b8e..0000000 --- a/frontend/js/labs/_pilots.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; -/* - * Пилотная регистрация в LabRegistry (Фаза 0 контент-движка). - * - * Доказывает паритет: каталог/открытие/теория этих 3 симуляций идут через реестр. - * Загружается ПОСЛЕДНИМ среди labs-скриптов, поэтому P_* (lab-glue.js), - * THEORY (lab-init.js) и _openXxx (graph/quadratic/pendulum.js) уже определены. - * preview задан функцией (ленивое вычисление в renderSims) — безопасно к порядку. - * - * В Фазе 1 регистрация переедет в сами sim-файлы, а этот файл будет удалён. - */ -(function () { - if (!window.LabRegistry) return; - var R = window.LabRegistry; - - R.register({ - id: 'graph', cat: 'math', - title: 'График функции', desc: 'Постройте и исследуйте графики функций', - preview: function () { return (typeof P_GRAPH !== 'undefined') ? P_GRAPH : ''; }, - theory: (typeof THEORY !== 'undefined') ? THEORY.graph : null, - open: function () { _openGraph(); }, - stop: function () { /* нет анимационного цикла */ }, - destroy: function () { /* нет ресурсов для освобождения */ } - }); - - R.register({ - id: 'quadratic', cat: 'math', - title: 'Квадратное уравнение', desc: 'Дискриминант, корни, теорема Виета', - preview: function () { return (typeof P_QUADRATIC !== 'undefined') ? P_QUADRATIC : ''; }, - theory: (typeof THEORY !== 'undefined') ? THEORY.quadratic : null, - open: function () { _openQuadratic(); }, - stop: function () { /* нет анимационного цикла */ }, - destroy: function () { /* нет ресурсов для освобождения */ } - }); - - R.register({ - id: 'pendulum', cat: 'phys', - title: 'Маятник', desc: 'Колебания, период, затухание', - preview: function () { return (typeof P_PENDULUM !== 'undefined') ? P_PENDULUM : ''; }, - theory: (typeof THEORY !== 'undefined') ? THEORY.pendulum : null, - open: function () { _openPendulum(); }, - stop: function () { if (typeof pendSim !== 'undefined' && pendSim && pendSim.stop) pendSim.stop(); }, - destroy: function () { if (typeof pendSim !== 'undefined' && pendSim && pendSim.stop) pendSim.stop(); } - }); -})(); diff --git a/frontend/js/labs/opticsbench.js b/frontend/js/labs/opticsbench.js index e072c2b..125f869 100644 --- a/frontend/js/labs/opticsbench.js +++ b/frontend/js/labs/opticsbench.js @@ -2545,7 +2545,7 @@ class BenchSim { this._drag = null; this._nextId = 1; // source: object arrow by default. `ang` (deg) aims point/single/laser/parallel. - this.source = { kind: 'object', xf: 0.07, yf: 0, h: 70, spread: 0.32, rays: 9, ang: 0 }; + this.source = { kind: 'object', xf: 0.07, yf: 0, h: 70, spread: 0.32, rays: 9, ang: 0, rayMode: 'char' }; // elements along the bench, positioned by x-fraction; centred on the axis this.elements = [ this._mk('lens', { xf: 0.40, f: 130, ap: 95 }), @@ -2602,7 +2602,7 @@ class BenchSim { this._redraw(); // canvas only — never rebuild the inspector mid-slider-drag } setSource(key, val) { - this.source[key] = (key === 'kind') ? val : +val; + this.source[key] = (key === 'kind' || key === 'rayMode') ? val : +val; // string keys vs numeric this._redraw(); } getSelected() { return this.elements.find(e => e.id === this.selectedId) || null; } @@ -2644,11 +2644,26 @@ class BenchSim { } 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, 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 => { - for (let i = 0; i < n; i++) push(sx, y0, -A + 2 * A * (i / (n - 1))); - }); + } else { // object arrow + const axis = this._ay(); // optical axis (lens centre / focus sit here) + const tipY = ay - this.source.h, baseY = ay; + const firstLens = this.elements.filter(e => e.type === 'lens').sort((a, b) => a.xf - b.xf)[0]; + if (this.source.rayMode === 'char' && firstLens) { + // textbook construction: 2–3 characteristic rays from the tip + axial ray from the base + const lensX = firstLens.xf * this.W, f = firstLens.f; + const aimAt = (tx, ty) => Math.atan2(ty - tipY, tx - sx); + push(sx, tipY, 0); // 1) parallel to axis → through far focus F' + push(sx, tipY, aimAt(lensX, axis)); // 2) through the optical centre → straight + const Fx = lensX - f; // front focal point + if (f > 0 && Fx > sx + 5) push(sx, tipY, aimAt(Fx, axis)); // 3) through F → emerges parallel + push(sx, baseY, 0); // base lies on the axis + } else { + // physical bundle: a fan from tip and base + const n = Math.max(2, this.source.rays | 0), A = this.source.spread; + [tipY, baseY].forEach(y0 => { + for (let i = 0; i < n; i++) push(sx, y0, -A + 2 * A * (i / (n - 1))); + }); + } } return rays; } @@ -4454,7 +4469,18 @@ function _benchPropsHTML() { h += _benchBtnRow(['object:Предмет', 'point:Точка', 'parallel:Параллель', 'single:Луч', 'laser:Лазер'], k => s.kind === k, k => "benchSourceKind('" + k + "')"); h += _benchCtl('Положение ↕', 0, 'yf', -0.9, 0.9, 0.02, +(s.yf || 0).toFixed(2), true); // vertical position (any kind) - if (s.kind === 'object') h += _benchCtl('Размер стрелки', 0, 'h', 20, 120, 2, s.h, true); + if (s.kind === 'object') { + h += _benchCtl('Размер стрелки', 0, 'h', 20, 120, 2, s.h, true); + // ray mode: textbook characteristic rays vs physical bundle + h += _benchBtnRow(['char:Характ. лучи', 'bundle:Пучок'], + k => (s.rayMode || 'char') === k, k => "benchSourceParam('rayMode','" + k + "');_benchUpdateUI()"); + if ((s.rayMode || 'char') === 'bundle') { + h += _benchCtl('Лучей', 0, 'rays', 3, 15, 1, s.rays, true); + h += _benchCtl('Раствор', 0, 'spread', 0.1, 0.6, 0.02, s.spread, true); + } else { + h += '
2–3 характеристических луча от вершины + осевой от основания (как в учебнике).
'; + } + } if (s.kind === 'point') h += _benchCtl('Раствор', 0, 'spread', 0.1, 0.6, 0.02, s.spread, true); if (s.kind !== 'object') h += _benchCtl('Угол°', 0, 'ang', -60, 60, 1, s.ang || 0, true); return h; diff --git a/frontend/lab.html b/frontend/lab.html index 2f20142..567b3fb 100644 --- a/frontend/lab.html +++ b/frontend/lab.html @@ -4847,7 +4847,7 @@ - +