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
+145 -1
View File
@@ -1,4 +1,4 @@
'use strict';
'use strict';
/* ═══════════════════════════════════════════════
GraphSim — interactive function plotter
@@ -491,3 +491,147 @@ class GraphSim {
this.onHover(this.hx, vals);
}
}
/* ─── lab UI init ─────────────────────────────────── */
function _openGraph() {
document.getElementById('sim-topbar-title').textContent = 'График функции';
_simShow('sim-graph');
_simShow('ctrl-graph');
_registerSimState('graph',
() => ({
fns: [0,1,2].map(i => ({ expr: document.getElementById(`fn${i}`)?.value || '', color: FN_COLORS[i] }))
}),
(st) => {
if (!Array.isArray(st.fns)) return;
st.fns.forEach((fn, i) => {
const el = document.getElementById(`fn${i}`);
if (el) { el.value = fn.expr; }
if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]);
});
}
);
if (_embedMode) _startStateEmit('graph');
requestAnimationFrame(() => requestAnimationFrame(() => {
if (!gSim) {
gSim = new GraphSim(document.getElementById('graph-canvas'));
gSim.onHover = updateInfoBar;
if (!document.getElementById('fn0').value.trim()) {
document.getElementById('fn0').value = 'sin(x)';
renderPreview(0);
gSim.fit();
gSim.setFn(0, 'sin(x)', FN_COLORS[0]);
return;
}
}
gSim.fit();
gSim.draw();
}));
}
/* ── projectile ── */
function toLatex(expr) {
if (!expr) return '';
return expr
// strip leading y= if typed
.replace(/^\s*y\s*=\s*/i, '')
// inverse trig (before sin/cos/tan)
.replace(/\barcsin\b/g, '\\arcsin').replace(/\barccos\b/g, '\\arccos')
.replace(/\b(arctan|arctg|atan|acos|asin)\b/g, (_, w) =>
w === 'asin' ? '\\arcsin' : w === 'acos' ? '\\arccos' : '\\arctan')
// trig
.replace(/\bctg\b/g, '\\cot').replace(/\btg\b/g, '\\tan')
.replace(/\b(sin|cos|tan)\b/g, '\\$1')
// log / exp
.replace(/\bln\b/g, '\\ln').replace(/\blog2\b/g, '\\log_2')
.replace(/\blog\b/g, '\\log').replace(/\bexp\b/g, '\\exp')
// special functions: f(inner) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> LaTeX form
.replace(/\bsqrt\(([^()]*)\)/g, '\\sqrt{$1}')
.replace(/\babs\(([^()]*)\)/g, '\\left|$1\\right|')
.replace(/\bfloor\(([^()]*)\)/g, '\\lfloor $1 \\rfloor')
.replace(/\bceil\(([^()]*)\)/g, '\\lceil $1 \\rceil')
.replace(/\b(round|sign)\b/g, '\\operatorname{$1}')
// constants
.replace(/\bpi\b/gi, '\\pi')
// power: wrap exponent in braces for multi-char
.replace(/\^(-?\d{2,})/g, '^{$1}')
// clean up multiplication
.replace(/([0-9])\s*\*\s*([a-zA-Z\\])/g, '$1\\,$2')
.replace(/\*/g, '\\cdot ');
}
function renderPreview(idx) {
const inp = document.getElementById('fn' + idx);
const prev = document.getElementById('fn' + idx + '-prev');
const raw = inp?.value?.trim() || '';
if (!raw || typeof katex === 'undefined') {
prev.innerHTML = ''; prev.classList.remove('has-content'); return;
}
try {
prev.innerHTML = katex.renderToString(toLatex(raw), {
throwOnError: false, strict: false, displayMode: false,
});
prev.classList.add('has-content');
} catch { prev.innerHTML = ''; prev.classList.remove('has-content'); }
}
/* debounced formula update */
const _debounce = {};
function updateFn(idx) {
clearTimeout(_debounce[idx]);
renderPreview(idx); // instant preview
_debounce[idx] = setTimeout(() => {
if (!gSim) return;
const raw = document.getElementById('fn' + idx).value;
const val = raw.replace(/^\s*y\s*=\s*/i, '');
const err = gSim.setFn(idx, val, FN_COLORS[idx]);
const errEl = document.getElementById('fn' + idx + '-err');
errEl.classList.toggle('show', !!err && !!val.trim());
}, 350);
}
function applyPreset(expr) {
for (let i = 0; i < 3; i++) {
const inp = document.getElementById('fn' + i);
if (!inp.value.trim()) {
inp.value = expr; updateFn(i); inp.focus(); return;
}
}
document.getElementById('fn0').value = expr; updateFn(0);
}
function clearAll() {
for (let i = 0; i < 3; i++) {
document.getElementById('fn' + i).value = '';
document.getElementById('fn' + i + '-prev').innerHTML = '';
document.getElementById('fn' + i + '-prev').classList.remove('has-content');
document.getElementById('fn' + i + '-err').classList.remove('show');
if (gSim) gSim.setFn(i, '', FN_COLORS[i]);
}
}
/* hover info bar */
function fmtVal(v) {
if (v === null || v === undefined) return '—';
if (!isFinite(v)) return '∞';
const abs = Math.abs(v);
if (abs === 0) return '0';
if (abs < 0.001 || abs >= 1e6) return v.toExponential(3);
return parseFloat(v.toPrecision(6)).toString();
}
function updateInfoBar(mx, vals) {
document.getElementById('info-x').textContent = mx !== null ? fmtVal(mx) : '—';
document.getElementById('info-y0').textContent = vals ? fmtVal(vals[0]) : '—';
document.getElementById('info-y1').textContent = vals ? fmtVal(vals[1]) : '—';
document.getElementById('info-y2').textContent = vals ? fmtVal(vals[2]) : '—';
}
/* ════════════════════════════════
МОЛЕКУЛЯРНАЯ ФИЗИКА (unified: gas + brownian + states + diffusion)
════════════════════════════════ */
let _molMode = 'gas'; // 'gas' | 'brownian' | 'states' | 'diffusion'