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 += '