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:
@@ -39,6 +39,11 @@ class ProjectileSim {
|
||||
this._path = null; // [{x, y, vx, vy, t}]
|
||||
this._pathTf = 0;
|
||||
|
||||
/* ── Energy bars widget ── */
|
||||
this._energyOn = false; // toggle state
|
||||
this._frictionWork = 0; // cumulative J lost to drag
|
||||
this._energyScale = 0; // max observed total (for stable scale)
|
||||
|
||||
/* animation state */
|
||||
this.t = 0;
|
||||
this.playing = false;
|
||||
@@ -124,6 +129,12 @@ class ProjectileSim {
|
||||
this.planetCompare = false; // show 3 planet trajectories simultaneously
|
||||
this.comparePlanets = ['earth', 'moon', 'mars']; // which 3
|
||||
|
||||
/* FBD toggle */
|
||||
this._fbdOn = false;
|
||||
|
||||
/* ── TimeControl (speed-only; projectile manages its own time) ── */
|
||||
this._tc = window.LSTimeControl ? new window.LSTimeControl(this) : null;
|
||||
|
||||
canvas.addEventListener('click', () => {
|
||||
if (this.onPlayPause) this.onPlayPause();
|
||||
});
|
||||
@@ -925,6 +936,13 @@ class ProjectileSim {
|
||||
|
||||
if (window.LabFX) LabFX.particles.update(rawDt);
|
||||
|
||||
/* TimeControl: if paused via TC, just redraw */
|
||||
if (this._tc && this._tc.paused) {
|
||||
this.draw(); this._emit();
|
||||
if (this.playing) this._tick();
|
||||
return;
|
||||
}
|
||||
|
||||
this._launchFlash = Math.max(0, this._launchFlash - rawDt * 2.5);
|
||||
|
||||
const prevT = this.t;
|
||||
@@ -932,7 +950,20 @@ class ProjectileSim {
|
||||
this._trail.push({ mx: cur.x, my: cur.y });
|
||||
if (this._trail.length > 80) this._trail.shift();
|
||||
|
||||
this.t += rawDt * this.speed;
|
||||
/* energy: accumulate drag work ΔW = F_drag · v · dt (approx) */
|
||||
if (this._energyOn && (this.drag || this.parachute)) {
|
||||
const spd = Math.sqrt(cur.vx * cur.vx + cur.vy * cur.vy);
|
||||
const rho = this.rho;
|
||||
const mass = Math.max(0.1, this.mass);
|
||||
const A = this._chuteOpen ? this.chuteArea : 0.00785;
|
||||
const Cd = this._chuteOpen ? this.chuteCd : this.Cd;
|
||||
const Fd = 0.5 * Cd * rho * A * spd * spd;
|
||||
this._frictionWork += Fd * spd * rawDt * this.speed;
|
||||
}
|
||||
|
||||
/* advance time; respect TC scale on top of existing speed multiplier */
|
||||
const tcScale = (this._tc && !this._tc.paused) ? this._tc.scale : 1;
|
||||
this.t += rawDt * this.speed * tcScale;
|
||||
const tf = this._curTFlight();
|
||||
if (this.t >= tf) {
|
||||
this.t = tf;
|
||||
@@ -1044,6 +1075,8 @@ class ProjectileSim {
|
||||
this._chuteOpen = this.parachute && this.chuteOpenHeight < 0;
|
||||
this._chuteOpenedTs = -999;
|
||||
this._chimeEmitted = false;
|
||||
this._frictionWork = 0;
|
||||
this._energyScale = 0;
|
||||
this._computePath();
|
||||
if (this.dualMode) {
|
||||
this._p2.t = 0;
|
||||
@@ -1718,10 +1751,80 @@ class ProjectileSim {
|
||||
this._drawInspector(ctx, tpx, tpy, PL, gy, W, H, PB, PT);
|
||||
}
|
||||
|
||||
/* ── 20. FBD overlay ── */
|
||||
if (this._fbdOn && window.LSPhysFX && this.t > 0) {
|
||||
const tf2 = this._curTFlight();
|
||||
const cur2 = this._curState(Math.min(this.t, tf2));
|
||||
const bx2 = tpx(cur2.x), by2 = tpy(Math.max(0, cur2.y));
|
||||
const spd2 = Math.sqrt(cur2.vx * cur2.vx + cur2.vy * cur2.vy);
|
||||
const FLEN = 48;
|
||||
|
||||
/* gravity — straight down */
|
||||
const mg = this.mass * this.g;
|
||||
LSPhysFX.drawForceArrow(ctx, bx2, by2, 0, FLEN, 'gravity',
|
||||
'mg=' + mg.toFixed(0) + 'Н');
|
||||
|
||||
/* drag — opposite velocity */
|
||||
if (this.drag && spd2 > 0.2) {
|
||||
const rho2 = this.rho || 1.225;
|
||||
const Fd = 0.5 * rho2 * this.Cd * 0.1 * spd2 * spd2;
|
||||
const dragLen = Math.min(FLEN, Fd * 6);
|
||||
if (dragLen > 3) {
|
||||
const dnx = -cur2.vx / spd2, dny = -cur2.vy / spd2;
|
||||
LSPhysFX.drawForceArrow(ctx, bx2, by2,
|
||||
dnx * dragLen, dny * dragLen,
|
||||
'drag', 'F_c=' + Fd.toFixed(1) + 'Н');
|
||||
}
|
||||
}
|
||||
|
||||
/* wind — horizontal */
|
||||
if (this.wind !== 0) {
|
||||
const windLen = Math.min(FLEN, Math.abs(this.wind) * 4);
|
||||
LSPhysFX.drawForceArrow(ctx, bx2, by2,
|
||||
Math.sign(this.wind) * windLen, 0,
|
||||
'applied', 'F_w=' + Math.abs(this.wind).toFixed(0) + 'м/с');
|
||||
}
|
||||
|
||||
/* elastic spring force — only when bounce just occurred */
|
||||
if (this.bounce) {
|
||||
const bounceElapsed = (performance.now() - this._impactTs) / 1000;
|
||||
if (bounceElapsed >= 0 && bounceElapsed < 0.25 && cur2.y <= 0.5) {
|
||||
LSPhysFX.drawForceArrow(ctx, bx2, by2, 0, -FLEN, 'elastic', 'F_упр');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Energy bars overlay ── */
|
||||
if (this._energyOn && window.LSPhysFX) {
|
||||
this._drawEnergyBarsProj(ctx, W, H);
|
||||
}
|
||||
|
||||
/* LabFX: particles overlay */
|
||||
if (window.LabFX) LabFX.particles.draw(this.ctx);
|
||||
}
|
||||
|
||||
/* ── Energy bars: projectile ── */
|
||||
|
||||
_drawEnergyBarsProj(ctx, W, H) {
|
||||
const tf = this._curTFlight();
|
||||
const cur = this._curState(Math.min(this.t, tf));
|
||||
const h = Math.max(0, cur.y); // height above launch (m)
|
||||
const spd = Math.sqrt(cur.vx * cur.vx + cur.vy * cur.vy);
|
||||
const m = Math.max(0.1, this.mass);
|
||||
const g = this.g;
|
||||
const ke = 0.5 * m * spd * spd;
|
||||
const pe = m * g * h;
|
||||
const fr = this._frictionWork;
|
||||
const tot = ke + pe + fr;
|
||||
/* stable scale */
|
||||
if (tot > this._energyScale) this._energyScale = tot;
|
||||
const scaleTotal = this._energyScale;
|
||||
|
||||
const PW = 188, MARGIN = 12;
|
||||
LSPhysFX.drawEnergyBars(ctx, W - PW - MARGIN, MARGIN, PW, 0,
|
||||
{ ke, pe, friction: fr, total: scaleTotal }, {});
|
||||
}
|
||||
|
||||
/* ── hover inspector ── */
|
||||
|
||||
_onMouseMove(e) {
|
||||
@@ -2011,6 +2114,7 @@ function _projArrow(ctx, x1, y1, x2, y2, color, lw) {
|
||||
pSim.onTargetUpdate = _projUpdateTargetHUD;
|
||||
const gc = document.getElementById('proj-graphs-canvas');
|
||||
if (gc) pSim.attachGraphsCanvas(gc);
|
||||
_projInjectTCBar(pSim);
|
||||
}
|
||||
pSim.fit();
|
||||
projParam(); // sync sliders → sim
|
||||
@@ -2019,6 +2123,24 @@ function _projArrow(ctx, x1, y1, x2, y2, color, lw) {
|
||||
}));
|
||||
}
|
||||
|
||||
function _projInjectTCBar(sim) {
|
||||
if (!window.LSTimeControl || !window.LSBuildTimeControlUI) return;
|
||||
var wrap = document.getElementById('sim-proj');
|
||||
if (!wrap || wrap.querySelector('.tc-bar')) return;
|
||||
|
||||
/* Only speed control — projectile uses analytical/pre-computed paths */
|
||||
/* TC.paused is checked in _tick. TC.scale multiplies rawDt * speed */
|
||||
var tc = sim._tc;
|
||||
|
||||
var tcBar = window.LSBuildTimeControlUI(sim, tc, { scrubSupported: false });
|
||||
|
||||
/* Note: "Следы" already exist in the projectile panel — don't duplicate */
|
||||
|
||||
var statsBar = wrap.querySelector('.proj-stats-bar');
|
||||
if (statsBar) wrap.insertBefore(tcBar, statsBar);
|
||||
else wrap.appendChild(tcBar);
|
||||
}
|
||||
|
||||
function projPlayPause() {
|
||||
if (!pSim) return;
|
||||
if (pSim.playing) {
|
||||
@@ -2396,5 +2518,19 @@ function _projArrow(ctx, x1, y1, x2, y2, color, lw) {
|
||||
pSim.draw();
|
||||
}
|
||||
|
||||
/* ── Energy toggle: projectile ── */
|
||||
function projToggleEnergy() {
|
||||
if (!pSim) return;
|
||||
pSim._energyOn = !pSim._energyOn;
|
||||
const on = pSim._energyOn;
|
||||
const btn = document.getElementById('proj-energy-btn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', on);
|
||||
btn.querySelector('span').textContent = on ? 'Энергия: Вкл' : 'Энергия: Выкл';
|
||||
}
|
||||
if (!on) { pSim._frictionWork = 0; pSim._energyScale = 0; }
|
||||
pSim.draw();
|
||||
}
|
||||
|
||||
/* ── collision ── */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user