feat(labs): wave 2 — depth features across 6 sims

Электрические цепи (circuit):
- Индуктивность L как новый компонент (1–1000 мГн, шорт в DC, jωL в AC)
- RLC preset для демонстрации резонанса
- Осциллограф: U(t)/I(t) для выбранного компонента, 100 sample, dual-axis
- Heatmap мощности: радиальный градиент halo от blue→red пропорционально P=UI

Стереометрия 3D (stereo):
- Сечение через 3 произвольные точки: pick на гранях/рёбрах/вершинах
- Плоскость + полигон пересечения с авто-определением типа (3–6-угольник) и площадью
- Step-by-step режим: визуализация P1→линия→P2→линия→P3→плоскость→сечение
- Поддержка всех solids (включая cylinder/cone через sampling fallback)

Планиметрия (geometry):
- Задачник framework: CHALLENGES[] с setup/check функциями
- 5 стартовых задач: серединный перпендикуляр, биссектриса, описанная окружность, ГМТ, касательная
- Авто-checker: толерантности ±0.5° для углов, ±1–5% для расстояний
- UI: collapsible панель с статус-иконками, конфетти + «Молодец!» на success

Электромагнитные поля (emfield):
- Preset «Тороид»: 16+16 проводов в концентрических кольцах
- Поверхность Гаусса: draggable круг, считает Φ = q_enc/ε₀, подсвечивает охваченные заряды
- Motional EMF: draggable rod, arrow-keys управление, считает ε = ∫(v×B)·dl

Химическая песочница (chemsandbox):
- Live-overlay с уравнением реакции: молекулярное / полное ионное / сокращённое ионное
- Coverage: 49/49 молекулярных, 34/49 ионных, 36/49 сокращённых
- Auto-hide через 5 сек, fade-in animation, цветовая кодировка типов

Волны и звук (waves):
- Doppler: source+observer drag, expanding wavefronts, f_obs формула, Mach cone при v>c
- Beats: f1+f2, sum waveform с envelope, индикация f_beat=|f1-f2|
- Spectrum (DFT): N=256 samples pure JS, bar-chart с пиками и labels, «Добавить гармонику»

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-23 12:48:14 +03:00
parent 7f75c96acd
commit 8f30a8cef6
8 changed files with 2367 additions and 36 deletions
+167
View File
@@ -167,6 +167,7 @@
<button class="zoom-btn circ-top-btn" id="ctool-resistor" onclick="circTool('resistor',this)" title="Резистор (R)" style="font-size:.6rem;font-weight:800">R</button>
<button class="zoom-btn circ-top-btn" id="ctool-battery" onclick="circTool('battery',this)" title="Батарея (B)" style="font-size:.6rem;font-weight:800">U</button>
<button class="zoom-btn circ-top-btn" id="ctool-capacitor" onclick="circTool('capacitor',this)" title="Конденсатор (C)" style="font-size:.6rem;font-weight:800">C</button>
<button class="zoom-btn circ-top-btn" id="ctool-inductor" onclick="circTool('inductor',this)" title="Катушка индуктивности (I)" style="font-size:.65rem"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 12 Q4 8 6 12 Q8 8 10 12 Q12 8 14 12"/><line x1="14" y1="12" x2="22" y2="12"/><line x1="2" y1="12" x2="2" y2="12"/></svg></button>
<button class="zoom-btn circ-top-btn" id="ctool-diode" onclick="circTool('diode',this)" title="Диод (D)" style="font-size:.75rem"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>|</button>
<button class="zoom-btn circ-top-btn" id="ctool-led" onclick="circTool('led',this)" title="LED" style="font-size:.6rem;font-weight:800">LED</button>
<button class="zoom-btn circ-top-btn" id="ctool-ac" onclick="circTool('ac',this)" title="AC источник" style="font-size:.65rem;font-weight:800">AC</button>
@@ -183,6 +184,13 @@
<button class="zoom-btn" onclick="cirSim&&cirSim.preset('clear')" title="Очистить">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
</button>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 2px"></div>
<button class="zoom-btn" id="ctool-heat" onclick="circToggleHeat()" title="Тепловая карта мощности" style="font-size:.6rem;font-weight:700">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 2C8 6 6 10 8 14c1 2 3 4 4 6"/><path d="M16 6c-2 3-3 6-1 9 1 1.5 1 3 0 4"/><path d="M8 6C6 9 5 12 7 15"/></svg>
</button>
<button class="zoom-btn" id="btn-osc-toggle" onclick="circToggleOsc()" title="Осциллограф U(t) / I(t)" style="font-size:.6rem;font-weight:700">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="6 16 8 10 11 14 13 8 16 16"/></svg>
</button>
</div>
<!-- reactions controls -->
@@ -510,6 +518,21 @@
</label>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Поверхность Гаусса</div>
<label class="tri-layer-row" id="em-gauss-row" onclick="emGaussToggle(this)" style="margin-bottom:6px">
<span class="tri-dot" style="background:#34d399;box-shadow:0 0 5px #34d399"></span>
<span class="tri-layer-name">Поток Гаусса &#934;&#8336;</span>
<span class="tri-layer-hint" style="color:#34d399">&#934;=q/&#949;&#8320;</span>
<span class="tri-toggle"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px"></span></span>
</label>
<div class="param-block" id="em-gauss-r-block" style="display:none;margin-bottom:10px">
<div class="param-header">
<span class="param-name">Радиус поверхности</span>
<span class="param-val" id="em-gaussR-val">70 пкс</span>
</div>
<input type="range" class="param-slider" id="sl-emGaussR" min="20" max="200" value="70" oninput="emGaussRChange()">
</div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты E</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="emPresetE('dipole')">Диполь &#177;</button>
@@ -583,6 +606,14 @@
<span class="tri-toggle"></span>
</label>
<div class="gp-section-title" style="margin-bottom:6px">ЭДС индукции</div>
<label class="tri-layer-row" id="em-rod-row" onclick="emRodToggle(this)" style="margin-bottom:6px">
<span class="tri-dot" style="background:#f59e0b;box-shadow:0 0 5px #f59e0b"></span>
<span class="tri-layer-name">Движущийся проводник</span>
<span class="tri-layer-hint" style="color:#f59e0b">&#949;=&#8747;vBdl</span>
<span class="tri-toggle"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px"></span></span>
</label>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты B</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="emPresetB('single')">Один провод</button>
@@ -591,6 +622,7 @@
<button class="proj-preset-chip" onclick="emPresetB('solenoid')">Соленоид</button>
<button class="proj-preset-chip" onclick="emPresetB('quadrupole')">Квадруполь</button>
<button class="proj-preset-chip" onclick="emPresetB('ring')">Кольцо</button>
<button class="proj-preset-chip" onclick="emPresetB('toroid')">Тороид</button>
</div>
</div><!-- /#em-ctrl-B -->
@@ -624,6 +656,10 @@
<div class="tri-stat-v" id="embar-curV" style="color:rgba(255,255,255,0.5)">&#8212;</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Курсор |B|</div>
<div class="tri-stat-v" id="embar-curB" style="color:var(--cyan)">&#8212;</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">&#934;&#8336; Гаусса</div>
<div class="tri-stat-v" id="embar-gauss" style="color:#34d399">&#8212;</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">ЭДС &#949;</div>
<div class="tri-stat-v" id="embar-rod" style="color:#f59e0b">&#8212;</div>
</div>
<div style="font-size:0.68rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:4px">
Клик &#8212; добавить &nbsp;&#183;&nbsp; ПКМ / 2&#215;клик &#8212; удалить<br>
@@ -647,6 +683,8 @@
<div class="pstat"><div class="pstat-label">|B| курсора</div><div class="pstat-val" id="embar-curB-bar" style="color:var(--cyan)">&#8212;</div></div>
<div class="pstat"><div class="pstat-label">Сила Ампера</div><div class="pstat-val" id="embar-ampere" style="color:#fbbf24">&#8212;</div></div>
<div class="pstat"><div class="pstat-label">Поток &#934;</div><div class="pstat-val" id="embar-flux" style="color:#34d399">&#8212;</div></div>
<div class="pstat"><div class="pstat-label">&#934;&#8336; Гаусса</div><div class="pstat-val" id="embar-gauss-bar" style="color:#34d399">&#8212;</div></div>
<div class="pstat"><div class="pstat-label">ЭДС &#949;</div><div class="pstat-val" id="embar-rod-bar" style="color:#f59e0b">&#8212;</div></div>
</div>
</div><!-- /#sim-emfield -->
@@ -1104,6 +1142,7 @@
<button class="proj-preset-chip circ-tool-btn" id="ptool-resistor" onclick="circTool('resistor',this)" data-tool="resistor">Резистор</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-battery" onclick="circTool('battery',this)" data-tool="battery">Батарея</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-capacitor" onclick="circTool('capacitor',this)" data-tool="capacitor">Конденсатор</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-inductor" onclick="circTool('inductor',this)" data-tool="inductor">Катушка L</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-diode" onclick="circTool('diode',this)" data-tool="diode">Диод</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-led" onclick="circTool('led',this)" data-tool="led">LED</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-ac" onclick="circTool('ac',this)" data-tool="ac">AC источник</button>
@@ -1140,6 +1179,14 @@
<input type="range" class="param-slider" id="sl-circC" min="10" max="1000" value="100" step="10" oninput="circCChange()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Индуктивность L</span>
<span class="param-val" id="circ-L-val">10 мГн</span>
</div>
<input type="range" class="param-slider" id="sl-circL" min="1" max="1000" value="10" step="1" oninput="circLChange()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота AC</span>
@@ -1159,6 +1206,7 @@
<button class="proj-preset-chip" onclick="circPreset('led')">LED</button>
<button class="proj-preset-chip" onclick="circPreset('rc')">RC-цепь</button>
<button class="proj-preset-chip" onclick="circPreset('ac')">AC-цепь</button>
<button class="proj-preset-chip" onclick="circPreset('rlc')">RLC-резонанс</button>
</div>
<div style="margin-top:auto;font-size:0.68rem;color:var(--text-3);text-align:center;line-height:1.7;padding-top:4px">
@@ -1170,6 +1218,10 @@
<div class="proj-canvas-outer">
<canvas id="circuit-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair"></canvas>
<div id="osc-panel" style="display:none;position:absolute;bottom:8px;right:8px;z-index:10;background:rgba(6,6,22,0.95);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:6px">
<div style="font-size:0.7rem;color:rgba(255,255,255,0.45);margin-bottom:4px;text-align:center">Осциллограф</div>
<canvas id="osc-canvas" width="300" height="180" style="display:block;border-radius:4px"></canvas>
</div>
</div>
</div><!-- /.sim-body-wrap -->
@@ -1991,6 +2043,22 @@
</div>
<div class="proj-canvas-outer">
<canvas id="chemsandbox-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:default"></canvas>
<!-- equation overlay: shown when reaction fires -->
<div id="chemsand-eq-overlay" class="chemsand-eq-overlay">
<div id="chemsand-eq-type" class="chemsand-eq-type"></div>
<div class="chemsand-eq-row chemsand-eq-row--mol">
<span class="chemsand-eq-label">Молекулярное</span>
<span id="chemsand-eq-mol" class="chemsand-eq-text chemsand-eq-text--mol"></span>
</div>
<div class="chemsand-eq-row chemsand-eq-row--full">
<span class="chemsand-eq-label">Полное ионное</span>
<span id="chemsand-eq-full" class="chemsand-eq-text chemsand-eq-text--full"></span>
</div>
<div class="chemsand-eq-row chemsand-eq-row--net">
<span class="chemsand-eq-label">Сокращённое ионное</span>
<span id="chemsand-eq-net" class="chemsand-eq-text chemsand-eq-text--net"></span>
</div>
</div>
</div>
</div>
<!-- Stats bar -->
@@ -2708,6 +2776,9 @@
<button class="wave-mode-btn" onclick="wavesMode('longitudinal',this)">Продольная</button>
<button class="wave-mode-btn" onclick="wavesMode('superposition',this)">Суперпозиция</button>
<button class="wave-mode-btn" onclick="wavesMode('standing',this)">Стоячая</button>
<button class="wave-mode-btn" onclick="wavesMode('doppler',this)">Доплер</button>
<button class="wave-mode-btn" onclick="wavesMode('beats',this)">Биения</button>
<button class="wave-mode-btn" onclick="wavesMode('spectrum',this)" style="grid-column:span 2">Спектр (ДПФ)</button>
</div>
<!-- Wave 1 -->
@@ -2773,6 +2844,63 @@
</div>
</div>
<!-- Doppler controls -->
<div id="waves-doppler-section" style="display:none">
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Эффект Доплера</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Скорость источника v<sub>s</sub></span>
<span class="param-val" id="waves-dopVs-val" style="color:var(--gold)">0.35c</span>
</div>
<input type="range" id="sl-waves-dopVs" class="param-slider" min="0" max="1.8" step="0.05" value="0.35" oninput="wavesParam('dopVs',this.value)" style="accent-color:#FFD166">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота источника f₀</span>
<span class="param-val" id="waves-f1-dop-val" style="color:var(--violet)"></span>
</div>
<input type="range" id="sl-waves-f1-dop" class="param-slider" min="0.3" max="4" step="0.1" value="1.0" oninput="wavesParam('f1',this.value)">
</div>
<div class="pp-hint" style="margin-bottom:8px">S — источник (перетащи), O — наблюдатель (перетащи)</div>
</div>
<!-- Beats controls -->
<div id="waves-beats-section" style="display:none">
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Биения</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота f₁</span>
<span class="param-val" id="waves-beatsF1-val" style="color:var(--violet)">440 Гц</span>
</div>
<input type="range" id="sl-waves-beatsF1" class="param-slider" min="100" max="1000" step="1" value="440" oninput="wavesParam('beatsF1',this.value)">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота f₂</span>
<span class="param-val" id="waves-beatsF2-val" style="color:var(--cyan)">444 Гц</span>
</div>
<input type="range" id="sl-waves-beatsF2" class="param-slider" min="100" max="1000" step="1" value="444" oninput="wavesParam('beatsF2',this.value)">
</div>
<div class="pp-hint" style="margin-bottom:8px">f<sub>бие</sub> = |f₁ − f₂|, огибающая — золотая кривая</div>
</div>
<!-- Spectrum controls -->
<div id="waves-spectrum-section" style="display:none">
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Спектр (ДПФ)</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Гармоника для добавления</span>
<span class="param-val" id="waves-specNewF-val" style="color:var(--pink)">5 Гц</span>
</div>
<input type="range" id="sl-waves-specNewF" class="param-slider" min="1" max="50" step="1" value="5" oninput="wavesParam('specNewF',this.value)" style="accent-color:var(--pink)">
</div>
<div style="display:flex;gap:6px;margin-bottom:8px">
<button class="preset-btn" onclick="wavesSpecAdd()" style="flex:1">Добавить гармонику</button>
<button class="preset-btn" onclick="wavesSpecClear()" style="flex:1;color:var(--pink)">Очистить</button>
</div>
<div class="pp-hint" style="margin-bottom:8px">ДПФ N=256, fs=100 Гц. Пики подписаны в Гц.</div>
</div>
<!-- Presets -->
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
@@ -3067,6 +3195,27 @@
</div>
<div id="angle-hint" style="font-size:0.63rem;color:rgba(255,255,255,0.38);margin-top:3px;line-height:1.4"></div>
<!-- ── Сечение через 3 точки ── -->
<div class="gp-section-title" style="margin-top:8px;margin-bottom:6px">Сечение через 3 точки</div>
<div class="st-tool-grid" style="margin-bottom:4px">
<button class="st-tool-btn st-tool-btn-wide" id="stereo-sect3p-btn" onclick="stereoSection3P(this)" title="Выбрать 3 точки — построить сечение">
<svg viewBox="0 0 24 24"><circle cx="5" cy="19" r="2.5" fill="currentColor"/><circle cx="12" cy="4" r="2.5" fill="currentColor"/><circle cx="19" cy="14" r="2.5" fill="currentColor"/><polyline points="5,19 12,4 19,14 5,19" fill="none" stroke-dasharray="3,2"/></svg>Сечение через 3 точки
</button>
</div>
<div class="st-action-grid" style="margin-top:3px">
<button class="st-action-btn" onclick="stereoSection3PClear()" style="grid-column:span 2">Сбросить сечение</button>
</div>
<div id="sect3p-hint" style="font-size:0.63rem;color:rgba(255,255,255,0.38);margin-top:3px;line-height:1.4"></div>
<div id="sect3p-info" style="font-size:0.7rem;margin-top:4px;line-height:1.6"></div>
<div class="st-toggle-row" style="margin-top:4px" onclick="stereoSection3PStepBy(this.querySelector('.st-toggle'))">
<span class="st-toggle-label"><svg viewBox="0 0 24 24"><line x1="4" y1="12" x2="20" y2="12"/><polyline points="13 5 20 12 13 19"/></svg>Пошагово</span>
<div class="st-toggle" id="stg-sect3p-step"></div>
</div>
<div class="st-action-grid" style="margin-top:3px">
<button class="st-action-btn" onclick="stereoSection3PPrevStep()">Назад</button>
<button class="st-action-btn" onclick="stereoSection3PNextStep()">Вперёд</button>
</div>
<!-- ── Формулы ── -->
<div class="gp-section-title" style="margin-top:8px;margin-bottom:4px">Формулы</div>
<div id="stereo-formulas" style="font-size:0.72rem;color:rgba(255,255,255,0.7);line-height:1.5;margin-bottom:6px"></div>
@@ -3472,6 +3621,11 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
Очистить
</button>
<button class="geo-challenge-toggle" id="geo-chall-toggle-btn" onclick="geoToggleChallengePanel()" title="Открыть задачник">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
Задачник
<span class="chall-count" id="geo-chall-count">0/5</span>
</button>
</div>
</div><!-- /.geo-panel -->
@@ -3486,6 +3640,19 @@
<button class="geo-del-btn geo-del-btn-hard" id="geo-del-hard">Со всеми зависимыми</button>
<button class="geo-del-btn geo-del-btn-cancel" id="geo-del-cancel">Отмена</button>
</div>
<!-- Challenge panel (slides in from right) -->
<div class="geo-challenge-panel" id="geo-challenge-panel">
<div class="geo-chall-header">
<span class="geo-chall-header-title">Задачник</span>
<button class="geo-chall-close" onclick="geoToggleChallengePanel()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="geo-chall-list" id="geo-chall-list">
<!-- Populated by JS -->
</div>
</div>
</div>
</div><!-- /.sim-body-wrap -->