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
+270 -1
View File
@@ -1,4 +1,4 @@
'use strict';
'use strict';
/**
* ReactionSim — Chemical reaction kinetics simulation.
@@ -616,3 +616,272 @@ class ReactionSim {
ctx.closePath();
}
}
/* ─── lab UI init ─────────────────────────────────── */
function _openChemistry(mode) {
document.getElementById('sim-topbar-title').textContent = 'Химические реакции';
_simShow('sim-chemistry');
_simShow('ctrl-chemistry');
if (mode) _chemMode = mode;
requestAnimationFrame(() => requestAnimationFrame(() => {
chemMode(_chemMode);
}));
}
function chemMode(mode, btn) {
_chemMode = mode;
const MODES = ['kinetics', 'flask', 'redox', 'ionex'];
const CANVASES = { kinetics: 'reactions-canvas', flask: 'flask-canvas', redox: 'redox-canvas', ionex: 'ionexchange-canvas' };
// toggle mode buttons
document.querySelectorAll('.chem-mode').forEach(b => b.classList.remove('active'));
const mb = document.getElementById('chem-mode-' + mode);
if (mb) mb.classList.add('active');
// toggle panels
MODES.forEach(m => {
const p = document.getElementById('chem-panel-' + m);
if (p) p.style.display = m === mode ? '' : 'none';
});
// toggle canvases
Object.entries(CANVASES).forEach(([m, cid]) => {
document.getElementById(cid).style.display = m === mode ? 'block' : 'none';
});
// toggle topbar tool groups
const modeToCtrl = { kinetics:'kin', flask:'flask', redox:'redox', ionex:'ionex' };
['kin', 'flask', 'redox', 'ionex'].forEach(k => {
const el = document.getElementById('ctrl-chem-' + k);
if (el) el.style.display = k === modeToCtrl[mode] ? 'contents' : 'none';
});
// stop all sims
if (reacSim) reacSim.stop();
if (flaskSim) flaskSim.stop();
if (rdxSim) rdxSim.stop();
if (ioxSim) ioxSim.stop();
// start the active one
if (mode === 'kinetics') {
const c = document.getElementById('reactions-canvas');
if (!reacSim) { reacSim = new ReactionSim(c); reacSim.onUpdate = _reacUpdateUI; }
reacSim.fit(); reacSim.start();
_reacUpdateUI(reacSim.info());
} else if (mode === 'flask') {
const c = document.getElementById('flask-canvas');
if (!flaskSim) { flaskSim = new FlaskSim(c); flaskSim.onUpdate = _flaskUpdateUI; }
flaskSim.fit(); flaskSim.start();
_flaskUpdateUI(flaskSim.info());
} else if (mode === 'redox') {
const c = document.getElementById('redox-canvas');
if (!rdxSim) { rdxSim = new RedoxSim(c); rdxSim.onUpdate = _redoxUpdateUI; }
rdxSim.fit(); rdxSim.draw();
_redoxUpdateUI(rdxSim.info());
} else if (mode === 'ionex') {
const c = document.getElementById('ionexchange-canvas');
if (!ioxSim) { ioxSim = new IonExSim(c); ioxSim.onUpdate = _ionexUpdateUI; }
ioxSim.fit(); ioxSim.draw();
_ionexUpdateUI(ioxSim.info());
}
}
function chemReset() {
if (_chemMode === 'kinetics' && reacSim) reacSim.reset();
if (_chemMode === 'flask' && flaskSim) flaskSim.reset();
if (_chemMode === 'redox') redoxReset();
if (_chemMode === 'ionex') ionexReset();
}
// _openReactions is now handled by _openChemistry + chemMode
function reacNChange() {
const v = +document.getElementById('sl-reacN').value;
document.getElementById('reac-N-val').textContent = v;
if (reacSim) reacSim.setN(v);
}
function reacTChange() {
const raw = +document.getElementById('sl-reacT').value;
const t = (raw / 10).toFixed(1);
document.getElementById('reac-T-val').textContent = t;
if (reacSim) reacSim.setT(+t);
}
function reacEaChange() {
const raw = +document.getElementById('sl-reacEa').value;
const ea = (raw / 10).toFixed(1);
document.getElementById('reac-Ea-val').textContent = ea;
if (reacSim) reacSim.setEa(+ea);
}
function reacMode(mode, el) {
if (reacSim) reacSim.setMode(mode);
document.querySelectorAll('.reac-mode-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
}
function reacPreset(name) {
if (!reacSim) return;
reacSim.preset(name);
// Sync sliders and mode buttons
document.getElementById('sl-reacN').value = reacSim.N;
document.getElementById('reac-N-val').textContent = reacSim.N;
document.getElementById('sl-reacT').value = Math.round(reacSim.T * 10);
document.getElementById('reac-T-val').textContent = reacSim.T.toFixed(1);
document.getElementById('sl-reacEa').value = Math.round(reacSim.Ea * 10);
document.getElementById('reac-Ea-val').textContent = reacSim.Ea.toFixed(1);
document.querySelectorAll('.reac-mode-btn').forEach(b => b.classList.remove('active'));
const mBtn = document.getElementById('rmode-' + reacSim.mode);
if (mBtn) mBtn.classList.add('active');
_reacUpdateUI(reacSim.info());
}
function reacTogglePause() {
if (!reacSim) return;
reacSim.toggleReaction();
const btn = document.getElementById('reac-pause-btn');
btn.innerHTML = reacSim.reactionOn ? '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Реакции';
}
function _reacUpdateUI(info) {
if (!info) return;
document.getElementById('chbar-l1').textContent = 'A молекул';
document.getElementById('chbar-v1').textContent = info.nA;
document.getElementById('chbar-l2').textContent = 'B молекул';
document.getElementById('chbar-v2').textContent = info.nB;
document.getElementById('chbar-l3').textContent = 'C продукт';
document.getElementById('chbar-v3').textContent = info.nC;
document.getElementById('chbar-l4').textContent = 'Реакций';
document.getElementById('chbar-v4').textContent = info.reactions;
document.getElementById('chbar-l5').textContent = 'Скорость';
document.getElementById('chbar-v5').textContent = info.rate > 0
? (info.rate * 30).toFixed(1) + '/с' : '—';
}
// _openFlask is now handled by _openChemistry('flask')
function flaskMetal(type, el) {
if (flaskSim) { flaskSim.setMetal(type); flaskSim.reset(); }
document.querySelectorAll('.flask-metal-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
}
function flaskAcid(type, el) {
if (flaskSim) flaskSim.setAcid(type);
document.querySelectorAll('.flask-acid-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
}
function flaskConcChange() {
const v = +document.getElementById('sl-flask-conc').value;
document.getElementById('flask-conc-val').textContent = v + '%';
if (flaskSim) flaskSim.setConc(v / 100);
}
function flaskTempChange() {
const v = +document.getElementById('sl-flask-temp').value;
document.getElementById('flask-temp-val').textContent = v + '°C';
if (flaskSim) flaskSim.setEnvTemp(v);
}
function flaskToggleFlame() {
if (!flaskSim) return;
flaskSim.toggleFlame();
const active = flaskSim._flameOn;
document.getElementById('flask-flame-btn').style.opacity = active ? '1' : '0.5';
document.getElementById('flask-flame-panel').style.opacity = active ? '1' : '0.5';
document.getElementById('flask-flame-panel').style.background = active ? 'rgba(239,71,111,0.22)' : '';
}
function flaskTogglePause() {
if (!flaskSim) return;
flaskSim.togglePause();
document.getElementById('flask-pause-btn').innerHTML = flaskSim._paused ? '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>' : '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
}
function _flaskUpdateUI(info) {
if (!info) return;
document.getElementById('chbar-l1').textContent = 'Металл';
document.getElementById('chbar-v1').textContent = info.metal;
document.getElementById('chbar-l2').textContent = 'Масса';
document.getElementById('chbar-v2').textContent = info.mass + ' г';
document.getElementById('chbar-l3').textContent = 'T (°C)';
document.getElementById('chbar-v3').textContent = info.temp + '°C';
document.getElementById('chbar-l4').textContent = 'pH';
document.getElementById('chbar-v4').textContent = info.pH;
document.getElementById('chbar-l5').textContent = 'H₂ (%)';
document.getElementById('chbar-v5').textContent = info.h2pct + '%';
}
// _openRedox is now handled by _openChemistry('redox')
function redoxRxn(id, el) {
document.querySelectorAll('.redox-rxn-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
if (rdxSim) { rdxSim.setReaction(id); }
}
function redoxStart() {
if (rdxSim) rdxSim.start();
}
function redoxReset() {
if (rdxSim) rdxSim.reset();
}
function _redoxUpdateUI(info) {
if (!info) return;
const phaseMap = { idle: 'ожидание', mixing: 'смешивание', reacting: 'реакция', done: 'завершена' };
document.getElementById('chbar-l1').textContent = 'Реакция';
document.getElementById('chbar-v1').textContent = info.rxn || '—';
document.getElementById('chbar-l2').textContent = 'Фаза';
document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
document.getElementById('chbar-l3').textContent = 'Прогресс';
document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
document.getElementById('chbar-l4').textContent = 'Электронов';
document.getElementById('chbar-v4').textContent = info.e + ' e⁻';
document.getElementById('chbar-l5').textContent = 'Тип';
document.getElementById('chbar-v5').innerHTML = info.phase === 'done' ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : '—';
}
// _openIonExchange is now handled by _openChemistry('ionex')
function ionexRxn(id, el) {
document.querySelectorAll('.ionex-rxn-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
if (ioxSim) { ioxSim.setReaction(id); }
}
function ionexStart() {
if (ioxSim) ioxSim.start();
}
function ionexReset() {
if (ioxSim) ioxSim.reset();
}
function _ionexUpdateUI(info) {
if (!info) return;
const phaseMap = { idle: 'ожидание', mixing: 'смешивание', pairing: 'реакция', done: 'завершена' };
const rxn = IonExSim.RXN[ioxSim.rxnId];
document.getElementById('chbar-l1').textContent = 'Реакция';
document.getElementById('chbar-v1').textContent = info.rxn || '—';
document.getElementById('chbar-l2').textContent = 'Фаза';
document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
document.getElementById('chbar-l3').textContent = 'Прогресс';
document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
document.getElementById('chbar-l4').textContent = 'Осадок';
document.getElementById('chbar-v4').textContent = info.precip > 0 ? info.precip + ' ч.' : '—';
document.getElementById('chbar-l5').textContent = 'Продукт';
document.getElementById('chbar-v5').textContent = rxn ? (rxn.sign || '—') : '—';
}
/* ════════════════════════════════
ЗАКОНЫ НЬЮТОНА
════════════════════════════════ */
/* ══════════════════════════════
DYNAMICS (unified Newton + Sandbox)
══════════════════════════════ */