refactor: distribute lab-init.js into 34 engine files

lab-init.js: 4098 -> 543 lines (infrastructure + THEORY only)

Each sim's _open*() + UI helpers moved to its engine file:
graph.js, projectile.js, collision.js, magnetic.js, triangle.js,
geometry.js, trigcircle.js, gas.js (molphys), coulomb.js, circuit.js,
reactions.js (chemistry), newton.js (dynamics), chemsandbox.js,
celldivision.js, photosynthesis.js, angrybirds.js, quadratic.js,
normaldist.js, graphtransform.js, pendulum.js, equilibrium.js,
thinlens.js, mirror.js, isoprocess.js, titration.js, refraction.js,
probability.js, bohratom.js, electrolysis.js, waves.js,
crystal.js, orbitals.js, stereo.js, hydrostatics.js

All 34 engine files syntax-checked OK.
This commit is contained in:
Maxim Dolgolyov
2026-05-08 14:54:54 +03:00
parent d5f77bb648
commit ae31e4c4e8
35 changed files with 3657 additions and 3589 deletions
+463 -1
View File
@@ -1,4 +1,4 @@
'use strict';
'use strict';
/* ════════════════════════════════════════════════════════════════
NewtonSim — три закона Ньютона
Закон I : A — скользящий блок, B — шар на нити
@@ -1202,3 +1202,465 @@ function _nwt_lighten(hex, d) {
const c = v => Math.max(0, Math.min(255, v));
return `rgb(${c((n >> 16) + d)},${c(((n >> 8) & 255) + d)},${c((n & 255) + d)})`;
}
/* ─── lab UI init ─────────────────────────────────── */
var newtonSim = null;
var sandboxSim = null;
let _dynMode = 'sandbox'; // current mode: 'sandbox' | 'law1' | 'law2' | 'law3'
function _openDynamics(preset) {
document.getElementById('sim-topbar-title').textContent = 'Динамика';
_simShow('sim-dynamics');
_simShow('ctrl-dynamics');
requestAnimationFrame(() => requestAnimationFrame(() => {
// init sandbox
const sbCanvas = document.getElementById('sandbox-canvas');
if (!sandboxSim) {
sandboxSim = new ForceSandboxSim(sbCanvas);
sandboxSim.onUpdate = _sbUpdateUI;
}
// init newton
const nwCanvas = document.getElementById('newton-canvas');
if (!newtonSim) {
newtonSim = new NewtonSim(nwCanvas);
newtonSim.onUpdate = _newtonUpdateUI;
}
// activate current mode
dynMode(_dynMode);
if (preset) setTimeout(() => sbPreset(preset), 120);
}));
}
function dynMode(mode, btn) {
_dynMode = mode;
const isSandbox = mode === 'sandbox';
// toggle mode buttons
document.querySelectorAll('.dyn-mode').forEach(b => b.classList.remove('active'));
const modeBtn = document.getElementById('dyn-mode-' + mode);
if (modeBtn) modeBtn.classList.add('active');
// toggle panels
document.getElementById('dyn-sandbox-panel').style.display = isSandbox ? '' : 'none';
document.getElementById('dyn-newton-panel').style.display = isSandbox ? 'none' : '';
// toggle canvases
document.getElementById('sandbox-canvas').style.display = isSandbox ? 'block' : 'none';
document.getElementById('newton-canvas').style.display = isSandbox ? 'none' : 'block';
// toggle topbar tool groups
document.getElementById('ctrl-dyn-sb').style.display = isSandbox ? 'contents' : 'none';
document.getElementById('ctrl-dyn-nw').style.display = isSandbox ? 'none' : 'contents';
if (isSandbox) {
// stop newton, start sandbox
if (newtonSim) newtonSim.stop();
if (sandboxSim) { sandboxSim.fit(); sandboxSim.start(); }
_sbUpdateUI(sandboxSim ? sandboxSim.info() : null);
} else {
// stop sandbox, switch newton law
if (sandboxSim) sandboxSim.stop();
const lawN = mode === 'law1' ? 1 : mode === 'law2' ? 2 : 3;
if (newtonSim) {
newtonSim.setLaw(lawN);
newtonSim.fit();
newtonSim.start();
_newtonSyncUI();
_newtonUpdateUI(newtonSim.info());
}
}
}
function dynPause() {
if (_dynMode === 'sandbox') {
if (sandboxSim) sandboxSim.togglePause();
} else {
if (newtonSim) newtonSim.togglePause();
}
}
function dynReset() {
if (_dynMode === 'sandbox') {
sbReset();
} else {
_resetNewtonScene();
}
}
const _NEWTON_SCENES = {
1: {
A: { desc: 'Закон инерции: тело скользит по поверхности. Нажми на canvas — толкни блок.', action: null },
B: { desc: 'Инерция в орбите: шар вращается на нити. Отруби нить — полетит по касательной!', action: '<svg class="ic" viewBox="0 0 24 24"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> Отрубить нить' },
C: { desc: 'Инерция в космосе: тело движется равномерно, нет сил — нет ускорения.', action: null },
},
2: {
A: { desc: 'Второй закон: F = ma. Прикладывай силу и следи за ускорением и скоростью.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить' },
B: { desc: 'Два тела, разные массы — одинаковая сила. Сравни ускорения!', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить' },
C: { desc: 'Второй закон: изменяй силу и массу ползунками, наблюдай в реальном времени.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить' },
},
3: {
A: { desc: 'Третий закон: пушка выстрелила — отдача. Импульс сохраняется!', action: 'Выстрел' },
B: { desc: 'Третий закон: два шара сталкиваются — силы равны и противоположны.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Столкнуть' },
C: { desc: 'Реактивное движение: ракета выбрасывает газ — летит в обратную сторону.', action: 'Двигатель' },
},
};
const _NEWTON_PRESETS = {
1: [
{ label: 'Космос', fn: 'space' },
{ label: 'Лёд', fn: 'ice' },
{ label: 'Асфальт', fn: 'asphalt' },
{ label: 'Резина', fn: 'rubber' },
],
2: [
{ label: 'Лёгкий', fn: 'light' },
{ label: 'Тяжёлый', fn: 'heavy' },
{ label: 'Сравнить', fn: 'compare' },
],
3: [
{ label: 'Большая пушка', fn: 'big_cannon' },
{ label: 'Маленькая', fn: 'small_cannon' },
{ label: 'Равные шары', fn: 'equal_balls' },
],
};
// _openNewton is now handled by _openDynamics + dynMode
// newtonLaw is now handled by dynMode('law1'/'law2'/'law3')
function newtonScene(s, topBtn, panelBtn) {
if (!newtonSim) return;
newtonSim.setScene(s);
document.querySelectorAll('.nscene-btn').forEach(b => {
b.classList.toggle('active', b.id === 'nscn-' + s || b.id === 'nscn-panel-' + s);
});
_newtonSyncUI();
_newtonUpdateUI(newtonSim.info());
}
function _newtonSyncUI() {
if (!newtonSim) return;
const law = newtonSim.law;
const scene = newtonSim.scene;
const sceneData = (_NEWTON_SCENES[law] || {})[scene] || {};
// description
const desc = document.getElementById('newton-scene-desc');
if (desc) desc.textContent = sceneData.desc || '';
// action button label
const lbl = sceneData.action || (law === 1 ? '<svg class="ic" viewBox="0 0 24 24"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> Нить' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Действие');
document.getElementById('newton-action-label').textContent = lbl;
document.getElementById('newton-action-top').textContent = lbl;
// show/hide sliders
document.getElementById('newton-mu-block').style.display = law === 1 && scene === 'A' ? '' : 'none';
document.getElementById('newton-mass1-block').style.display = (law === 2 || law === 3) ? '' : 'none';
document.getElementById('newton-mass2-block').style.display = law === 3 ? '' : 'none';
document.getElementById('newton-force-block').style.display = law === 2 ? '' : 'none';
// sync slider values from sim
document.getElementById('sl-newton-mu').value = newtonSim.mu;
document.getElementById('newton-mu-val').textContent = newtonSim.mu.toFixed(2);
document.getElementById('sl-newton-m1').value = newtonSim.mass1;
document.getElementById('newton-m1-val').textContent = newtonSim.mass1 + ' кг';
document.getElementById('sl-newton-m2').value = newtonSim.mass2;
document.getElementById('newton-m2-val').textContent = newtonSim.mass2 + ' кг';
document.getElementById('sl-newton-F').value = newtonSim.force;
document.getElementById('newton-F-val').textContent = newtonSim.force + ' Н';
// sync scene highlight buttons in both topbar and panel
['A','B','C'].forEach(s => {
const tb = document.getElementById('nscn-' + s);
const pb = document.getElementById('nscn-panel-' + s);
const on = s === scene;
if (tb) tb.classList.toggle('active', on);
if (pb) pb.classList.toggle('active', on);
});
// presets
const presetsEl = document.getElementById('newton-presets');
const presets = _NEWTON_PRESETS[law] || [];
presetsEl.innerHTML = presets.map(p =>
`<button class="proj-preset-chip" onclick="newtonPreset('${p.fn}')">${p.label}</button>`
).join('');
// scene B/C visibility for law I (B = orbital, C = space — but law I only has A,B)
// scene C doesn't exist for law I/II panel scene picker visibility
const cBtn = document.getElementById('nscn-panel-C');
const cTopBtn = document.getElementById('nscn-C');
const showC = law === 3;
if (cBtn) cBtn.style.display = showC ? '' : 'none';
if (cTopBtn) cTopBtn.style.display = showC ? '' : 'none';
const bBtn = document.getElementById('nscn-panel-B');
const bTopBtn = document.getElementById('nscn-B');
const showB = law !== 2 || true; // law 2 has compare scene B
if (bBtn) bBtn.style.display = '';
if (bTopBtn) bTopBtn.style.display = '';
}
function newtonAction() {
if (!newtonSim) return;
const law = newtonSim.law;
const scene = newtonSim.scene;
if (law === 1 && scene === 'B') newtonSim.cutString();
else if (law === 2) newtonSim.startL2();
else if (law === 3 && scene === 'A') newtonSim.fireCannon();
else if (law === 3 && scene === 'B') newtonSim._reset3B ? newtonSim._reset3B() : null;
else if (law === 3 && scene === 'C') newtonSim.toggleRocket();
_newtonUpdateUI(newtonSim.info());
}
function _resetNewtonScene() {
if (!newtonSim) return;
const law = newtonSim.law;
const scene = newtonSim.scene;
if (law === 1 && scene === 'A') newtonSim.preset('ice');
else if (law === 1) newtonSim.setScene(scene);
else if (law === 2) newtonSim.resetL2 ? newtonSim.resetL2() : newtonSim.setScene(scene);
else newtonSim.setScene(scene);
_newtonUpdateUI(newtonSim.info());
}
function newtonMuChange() {
const v = +document.getElementById('sl-newton-mu').value;
document.getElementById('newton-mu-val').textContent = v.toFixed(2);
if (newtonSim) newtonSim.setMu(v);
}
function newtonMass1Change() {
const v = +document.getElementById('sl-newton-m1').value;
document.getElementById('newton-m1-val').textContent = v + ' кг';
if (newtonSim) newtonSim.setMass1(v);
}
function newtonMass2Change() {
const v = +document.getElementById('sl-newton-m2').value;
document.getElementById('newton-m2-val').textContent = v + ' кг';
if (newtonSim) newtonSim.setMass2(v);
}
function newtonForceChange() {
const v = +document.getElementById('sl-newton-F').value;
document.getElementById('newton-F-val').textContent = v + ' Н';
if (newtonSim) newtonSim.setForce(v);
}
function newtonPreset(name) {
if (!newtonSim) return;
newtonSim.preset(name);
_newtonSyncUI();
_newtonUpdateUI(newtonSim.info());
}
function _newtonUpdateUI(info) {
if (!info) return;
const law = info.law;
const scene = info.scene;
if (law === 1 && scene === 'A') {
document.getElementById('dbar-l1').textContent = 'Закон I-A';
document.getElementById('dbar-v1').textContent = 'Скольжение';
document.getElementById('dbar-l2').textContent = 'Скорость';
document.getElementById('dbar-v2').textContent = info.v + ' м/с';
document.getElementById('dbar-l3').textContent = 'Сила трения';
document.getElementById('dbar-v3').textContent = info.fFr + ' Н';
document.getElementById('dbar-l4').textContent = 'Масса';
document.getElementById('dbar-v4').textContent = info.m + ' кг';
document.getElementById('dbar-l5').textContent = 'μ';
document.getElementById('dbar-v5').textContent = info.mu;
} else if (law === 1) {
document.getElementById('dbar-l1').textContent = 'Закон I-B';
document.getElementById('dbar-v1').textContent = info.cut ? 'Нить срублена' : 'Вращение';
document.getElementById('dbar-l2').textContent = 'Скорость';
document.getElementById('dbar-v2').textContent = info.v + ' м/с';
document.getElementById('dbar-l3').textContent = '';
document.getElementById('dbar-v3').textContent = '—';
document.getElementById('dbar-l4').textContent = '';
document.getElementById('dbar-v4').textContent = '—';
document.getElementById('dbar-l5').textContent = '';
document.getElementById('dbar-v5').textContent = '—';
} else if (law === 2) {
document.getElementById('dbar-l1').textContent = 'Закон II';
document.getElementById('dbar-v1').textContent = 'F = ma';
document.getElementById('dbar-l2').textContent = 'Сила F';
document.getElementById('dbar-v2').textContent = info.F + ' Н';
document.getElementById('dbar-l3').textContent = 'Масса m';
document.getElementById('dbar-v3').textContent = info.m + ' кг';
document.getElementById('dbar-l4').textContent = 'Ускор. a';
document.getElementById('dbar-v4').textContent = info.a + ' м/с²';
document.getElementById('dbar-l5').textContent = 'Скорость';
document.getElementById('dbar-v5').textContent = info.v + ' м/с';
} else if (scene === 'A') {
document.getElementById('dbar-l1').textContent = 'Закон III-A';
document.getElementById('dbar-v1').textContent = 'Пушка';
document.getElementById('dbar-l2').textContent = 'v снаряда';
document.getElementById('dbar-v2').textContent = info.vBall !== '—' ? info.vBall + ' м/с' : '—';
document.getElementById('dbar-l3').textContent = 'v пушки';
document.getElementById('dbar-v3').textContent = info.vCannon + ' м/с';
document.getElementById('dbar-l4').textContent = 'm снаряда';
document.getElementById('dbar-v4').textContent = info.m1 + ' кг';
document.getElementById('dbar-l5').textContent = 'm пушки';
document.getElementById('dbar-v5').textContent = info.m2 + ' кг';
} else if (scene === 'B') {
document.getElementById('dbar-l1').textContent = 'Закон III-B';
document.getElementById('dbar-v1').textContent = 'Удар';
document.getElementById('dbar-l2').textContent = 'p₁';
document.getElementById('dbar-v2').textContent = info.p1 + ' кг·м/с';
document.getElementById('dbar-l3').textContent = 'p₂';
document.getElementById('dbar-v3').textContent = info.p2 + ' кг·м/с';
document.getElementById('dbar-l4').textContent = 'p суммарный';
document.getElementById('dbar-v4').textContent = info.pt + ' кг·м/с';
document.getElementById('dbar-l5').textContent = '';
document.getElementById('dbar-v5').textContent = '—';
} else {
document.getElementById('dbar-l1').textContent = 'Закон III-C';
document.getElementById('dbar-v1').textContent = 'Ракета';
document.getElementById('dbar-l2').textContent = 'Ускорение';
document.getElementById('dbar-v2').textContent = info.a + ' м/с²';
document.getElementById('dbar-l3').textContent = 'Скорость';
document.getElementById('dbar-v3').textContent = info.v + ' м/с';
document.getElementById('dbar-l4').textContent = 'Масса';
document.getElementById('dbar-v4').textContent = info.m + ' кг';
document.getElementById('dbar-l5').textContent = 'Топливо';
document.getElementById('dbar-v5').textContent = info.fuel + '%';
}
}
// _openSandbox is now handled by _openDynamics + dynMode
function sbTool(t, btn) {
if (!sandboxSim) return;
sandboxSim.tool = t;
sandboxSim._springStart = null;
sandboxSim._ropeStart = null;
document.querySelectorAll('.sb-tool-btn').forEach(b => b.classList.toggle('active', b.id === 'sbt-' + t));
document.querySelectorAll('.sb-panel-tool').forEach(b => b.classList.toggle('active', b.id === 'sbpt-' + t));
const canvas = document.getElementById('sandbox-canvas');
canvas.style.cursor = t === 'erase' ? 'not-allowed'
: (t === 'spring' || t === 'rope') ? 'cell'
: t === 'anchor' ? 'copy'
: 'crosshair';
document.getElementById('sb-spring-block').style.display = t === 'spring' ? '' : 'none';
}
function sbSpringKChange() {
const v = +document.getElementById('sl-sb-springk').value;
document.getElementById('sb-springk-val').textContent = v + ' Н/м';
if (sandboxSim) sandboxSim.newSpringK = v;
}
function sbForceMode(m, btn) {
if (!sandboxSim) return;
sandboxSim.forceMode = m;
document.querySelectorAll('.sb-fmode').forEach(b => b.classList.toggle('active', b.id === 'sbfm-' + m));
}
function sbMassChange() {
const v = +document.getElementById('sl-sb-mass').value;
document.getElementById('sb-mass-val').textContent = v + ' кг';
if (sandboxSim) sandboxSim.newMass = v;
}
function sbRestChange() {
const v = +document.getElementById('sl-sb-rest').value;
document.getElementById('sb-rest-val').textContent = v.toFixed(2);
if (sandboxSim) sandboxSim.newRestitution = v;
}
function sbFloorMuChange() {
const v = +document.getElementById('sl-sb-floormu').value;
document.getElementById('sb-floormu-val').textContent = v.toFixed(2);
if (sandboxSim) sandboxSim.floorMu = v;
}
function sbWorldToggle() {
if (!sandboxSim) return;
sandboxSim.gravity = document.getElementById('sb-gravity').checked;
sandboxSim.hasFloor = document.getElementById('sb-floor').checked;
sandboxSim.hasWalls = document.getElementById('sb-walls').checked;
sandboxSim.airDrag = document.getElementById('sb-airdrag').checked;
}
function sbRampToggle() {
if (!sandboxSim) return;
const on = document.getElementById('sb-ramp').checked;
sandboxSim.setRamp(on);
document.getElementById('sb-ramp-block').style.display = on ? '' : 'none';
}
function sbAngleChange() {
const v = +document.getElementById('sl-sb-angle').value;
document.getElementById('sb-angle-val').textContent = v + '°';
if (sandboxSim) sandboxSim.setRampAngle(v);
}
function sbRampMuChange() {
const v = +document.getElementById('sl-sb-rampmu').value;
document.getElementById('sb-rampmu-val').textContent = v.toFixed(2);
if (sandboxSim) sandboxSim.setRampMu(v);
}
function sbDecompToggle() {
if (!sandboxSim) return;
sandboxSim.showDecomp = document.getElementById('sb-decomp').checked;
}
function sbDisplayToggle() {
if (!sandboxSim) return;
sandboxSim.showForces = document.getElementById('sb-forces').checked;
sandboxSim.showVelocity = document.getElementById('sb-vel').checked;
sandboxSim.showFBD = document.getElementById('sb-fbd').checked;
sandboxSim.showEnergy = document.getElementById('sb-energy').checked;
sandboxSim.showTrail = document.getElementById('sb-trail').checked;
}
function sbTimeScale(v, btn) {
if (!sandboxSim) return;
sandboxSim.timeScale = v;
document.querySelectorAll('.sb-time').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
}
function sbPreset(name) {
if (!sandboxSim) return;
sandboxSim.preset(name);
// sync world checkboxes
document.getElementById('sb-gravity').checked = sandboxSim.gravity;
document.getElementById('sb-floor').checked = sandboxSim.hasFloor;
document.getElementById('sb-walls').checked = sandboxSim.hasWalls;
document.getElementById('sb-airdrag').checked = sandboxSim.airDrag;
document.getElementById('sl-sb-floormu').value = sandboxSim.floorMu;
document.getElementById('sb-floormu-val').textContent = sandboxSim.floorMu.toFixed(2);
// sync ramp
document.getElementById('sb-ramp').checked = sandboxSim.ramp;
document.getElementById('sb-ramp-block').style.display = sandboxSim.ramp ? '' : 'none';
document.getElementById('sl-sb-angle').value = sandboxSim.rampAngle;
document.getElementById('sb-angle-val').textContent = sandboxSim.rampAngle + '°';
document.getElementById('sl-sb-rampmu').value = sandboxSim.rampMu;
document.getElementById('sb-rampmu-val').textContent = sandboxSim.rampMu.toFixed(2);
_sbUpdateUI(sandboxSim.info());
}
function sbReset() {
if (!sandboxSim) return;
sandboxSim.reset();
_sbUpdateUI(sandboxSim.info());
}
function _sbUpdateUI(info) {
if (!info) return;
document.getElementById('dbar-l1').textContent = 'Тел / связей';
document.getElementById('dbar-v1').textContent = info.bodies + ' / ' + (info.springs + info.ropes);
document.getElementById('dbar-l2').textContent = 'KE (Дж)';
document.getElementById('dbar-v2').textContent = info.KE;
document.getElementById('dbar-l3').textContent = 'PE (Дж)';
document.getElementById('dbar-v3').textContent = info.PE;
document.getElementById('dbar-l4').textContent = 'ΣF';
document.getElementById('dbar-v4').textContent = info.netF;
document.getElementById('dbar-l5').textContent = 'Время';
document.getElementById('dbar-v5').textContent = info.time + ' с';
}
/* ── chem sandbox ── */