feat(opticsbench): режим лучей предмета — характеристические vs пучок

- источник «Предмет»: тумблер «Характ. лучи» (по умолчанию) / «Пучок»
- характеристические: 3 луча от вершины (параллельный→F', через центр,
  через F→параллельно) + осевой от основания — как в учебнике; проверено
  численно (F'=lensX+f, центр прямо, через F выход параллелен)
- пучок: прежний физичный веер + ползунок «Лучей» (густота) и «Раствор»
- setSource: rayMode как строковый ключ; bump opticsbench.js?v=9

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 13:28:15 +03:00
parent 8a9ff304f2
commit 6a3d1e04d0
3 changed files with 35 additions and 54 deletions
-45
View File
@@ -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(); }
});
})();
+34 -8
View File
@@ -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: 23 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 += '<div class="pp-hint">23 характеристических луча от вершины + осевой от основания (как в учебнике).</div>';
}
}
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;
+1 -1
View File
@@ -4847,7 +4847,7 @@
<script src="/js/labs/graphtransform.js"></script>
<script src="/js/labs/pendulum.js"></script>
<script src="/js/labs/equilibrium.js"></script>
<script src="/js/labs/opticsbench.js?v=8"></script>
<script src="/js/labs/opticsbench.js?v=9"></script>
<script src="/js/labs/isoprocess.js"></script>
<script src="/js/labs/titration.js"></script>
<script src="/js/labs/probability.js"></script>