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:
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user