feat(labs): универсальные инструменты для физических симуляций (Раунд 2)

ФУНДАМЕНТ — frontend/js/labs/_phys_visuals.js (новый файл, 871 строк):
- LSPhysFX.FORCE_COLORS: стандарт 10 цветов (mg красный, N циан, F_тр оранжевый,
  T фиолетовый, F_упр зелёный, F_сопр серый, F_прил жёлтый, импульс, v, a)
- drawVector / drawForceArrow / drawSpring / drawRope / drawSurface
- drawCoordSystem / drawScale / drawClock / drawAngleArc / drawPivot
- drawEnergyBars (KE/PE/W_тр/E_упр стеком с авто-масштабированием)
- LSTimeControl class (pause/play/0.1×-5×/scrubber для одного DOF)
- LSMotionTrail class (gradient line с alpha fade)
- LSBuildTimeControlUI helper для DOM-UI бара

ГРАФИКИ — frontend/js/labs/_graph_panel.js (новый файл, 415 строк):
- LSGraphPanel: 3 stacked time-series плота, авто-scale, freeze/export PNG
- LSGraphPanelUI: overlay-виджет 320×248 в углу canvas, body-selector,
  кнопки Сброс/Стоп/PNG download

FBD (свободные силовые диаграммы) интегрированы в:
- projectile.js: mg + drag + wind + elastic (bounce)
- pendulum.js: mg + T (math), mg + F_упр (spring vert/horiz)
- collision.js: стрелки скорости каждого шара + flash импульса
- newton.js: все сцены законов I-III + новые Атвуд/наклон/скатывание
- forcesandbox.js: gravity/N/friction/spring/applied на каждом теле

ENERGY BARS интегрированы в 5 сим с расчётами:
- projectile: ΔE_drag = F_d·v·dt (cumulative)
- pendulum: для math/spring/double/physical с учётом γ-затухания
- collision: KE loss при каждом столкновении
- newton: все 8 сцен включая Атвуд + наклон + скатывание (с моментом инерции)
- forcesandbox: + E_упр от пружин

GRAPHS PANEL — в 5 сим:
- pendulum: θ/ω/E (режим-aware)
- collision: |v₁|, |v₂|, v_цм
- newton: x/v/a (зависит от закона)
- forcesandbox: x/|v|/|a| выбранного тела
- hydrostatics: depth/vy/submergedFrac (только Архимед)

TIME CONTROL + MOTION TRAILS в 5 сим:
- pendulum (полный scrubber для math), collision, newton, forcesandbox (speed+pause)
- projectile (layered speed+pause, свой trail сохранён)
- LSMotionTrail на bob/балах/блоках с alpha gradient

Заменено рисование пружин на LSPhysFX.drawSpring везде.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-26 14:37:48 +03:00
parent e46548d06b
commit 7a323f8fe0
10 changed files with 2536 additions and 39 deletions
+47
View File
@@ -65,6 +65,10 @@ class HydroSim {
this._waterLevel = 0.60;
this._bodyShape = 'rect';
/* -- GraphPanel widget -- */
this._graphsOn = false;
this._graphUI = null;
this._bindEvents();
this._ro = new ResizeObserver(() => this.fit());
this._ro.observe(canvas.parentElement || canvas);
@@ -179,6 +183,16 @@ class HydroSim {
if (window.LabFX) LabFX.particles.update(dt);
this._update(t);
this._draw(t);
if (window.LSGraphPanel && this._graphsOn && this._graphUI && this.mode === 'archimedes' && this._archReady) {
const bodies = this._bodies;
if (bodies && bodies.length > 0) {
const b = bodies[0];
const submersion = b ? Math.max(0, Math.min(1, b.submergedFrac || 0)) : 0;
const depth = b ? (b.y || 0) : 0;
const vel = b ? (b.vy || 0) : 0;
this._graphUI.push(this._t / 1000, [depth, vel, submersion]);
}
}
if (t - this._lastNotify > 120) { this._lastNotify = t; this._notify(); }
}
@@ -1382,6 +1396,28 @@ class HydroSim {
.map(([a,b]) => p(a,b,t).toString(16).padStart(2,'0')).join('');
}
_notify() { if (this.onUpdate) try { this.onUpdate(this.getInfo()); } catch {} }
toggleGraphs(canvasWrap) {
if (!window.LSGraphPanelUI) return false;
this._graphsOn = !this._graphsOn;
if (this._graphsOn) {
this._simT = 0;
this._graphUI = new GraphPanelUI(canvasWrap, {
maxPoints: 300,
traces: ['depth', 'vy', 'sub'],
labels: ['Глубина', 'v', 'Погружение'],
units: ['пкс', 'пкс/с', '0..1'],
colors: ['#06D6E0', '#FFD166', '#7BF5A4'],
toggleBtnId: 'btn-hydro-graphs',
title: 'Погружение'
});
this._graphUI.isOn = true;
this._graphUI._build();
} else {
if (this._graphUI) { this._graphUI._destroy(); this._graphUI = null; }
}
return this._graphsOn;
}
}
/* ─── lab UI init ─────────────────────────────────── */
@@ -1423,6 +1459,8 @@ class HydroSim {
if (el) el.style.display = 'none';
if (el2) el2.style.display = 'none';
});
const gpRow = document.getElementById('hydro-graphs-row');
if (gpRow) gpRow.style.display = mode === 'archimedes' ? '' : 'none';
if (mode === 'archimedes') {
const a = document.getElementById('hydro-panel-mat');
const b = document.getElementById('hydro-arch-ctrl');
@@ -1467,6 +1505,15 @@ class HydroSim {
});
}
function hydroToggleGraphs() {
if (!hydroSim || typeof hydroSim.toggleGraphs !== 'function') return;
const canvasWrap = document.getElementById('hydro-canvas-wrap');
if (!canvasWrap) return;
const on = hydroSim.toggleGraphs(canvasWrap);
const btn = document.getElementById('btn-hydro-graphs');
if (btn) btn.classList.toggle('active', on);
}
function hydroSetVessels(n, btn) {
if (hydroSim) hydroSim.setNumVessels(n);
document.querySelectorAll('.hydro-nv').forEach(b => b.classList.remove('active'));