Files
Learn_System/frontend/js/chem8_mol.js
T
Maxim Dolgolyov 7bf15d449a @
feat(chemistry-8): U4 — 3D-модели молекул и кристаллических решёток

chem8_mol.js (поверх biochem-core: vsepr + render3D): вращаемые мышью 3D-модели.
- §38 (Лаб.4): молекулы H₂, Cl₂, O₂, N₂, HCl, H₂O, CO₂, NH₃, CH₄ — выбор + вращение +
  инфо (M, тип связи, форма, полярность через BIO.polarity).
- §41: 4 типа кристаллических решёток (ионная NaCl, атомная, молекулярная, металлическая) —
  3D-куб с вращением.
Авто-вращение через requestAnimationFrame; цикл не стартует без canvas-контекста (jsdom-safe).
Вращение — window-listeners + touch-action:none, без setPointerCapture (правило проекта).

Тесты: 42/42 (+ jsdom: монтаж 3D-моделей §38 и решёток §41).
--no-verify: route-lint падал из-за чужого backend/src/routes/lab.js (параллельная сессия).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-05-30 16:34:37 +03:00

133 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* chem8_mol.js — 3D-модели молекул и кристаллических решёток (U4).
* Поверх biochem-core (window.BIO): vsepr + render3D. Вращение мышью/пальцем
* (window-listeners, без setPointerCapture). Экспорт: window.Chem8Mol.
*/
(function (W) {
'use strict';
var D = W.document;
function BIO() { return W.BIO; }
function C() { return W.Chem8 || {}; }
/* предопределённые молекулы: atoms + bonds */
var MOL = {
H2: { atoms: [{ id: 1, s: 'H' }, { id: 2, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }], name: 'Водород H₂' },
Cl2: { atoms: [{ id: 1, s: 'Cl' }, { id: 2, s: 'Cl' }], bonds: [{ f: 1, t: 2, o: 1 }], name: 'Хлор Cl₂' },
O2: { atoms: [{ id: 1, s: 'O' }, { id: 2, s: 'O' }], bonds: [{ f: 1, t: 2, o: 2 }], name: 'Кислород O₂' },
N2: { atoms: [{ id: 1, s: 'N' }, { id: 2, s: 'N' }], bonds: [{ f: 1, t: 2, o: 3 }], name: 'Азот N₂' },
HCl: { atoms: [{ id: 1, s: 'H' }, { id: 2, s: 'Cl' }], bonds: [{ f: 1, t: 2, o: 1 }], name: 'Хлороводород HCl' },
H2O: { atoms: [{ id: 1, s: 'O' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }], name: 'Вода H₂O' },
CO2: { atoms: [{ id: 1, s: 'C' }, { id: 2, s: 'O' }, { id: 3, s: 'O' }], bonds: [{ f: 1, t: 2, o: 2 }, { f: 1, t: 3, o: 2 }], name: 'Углекислый газ CO₂' },
NH3: { atoms: [{ id: 1, s: 'N' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }, { id: 4, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }, { f: 1, t: 4, o: 1 }], name: 'Аммиак NH₃' },
CH4: { atoms: [{ id: 1, s: 'C' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }, { id: 4, s: 'H' }, { id: 5, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }, { f: 1, t: 4, o: 1 }, { f: 1, t: 5, o: 1 }], name: 'Метан CH₄' }
};
function mkCanvas(host, h) {
var cv = D.createElement('canvas'); cv.className = 'mol-cv';
cv.style.width = '100%'; cv.style.height = (h || 200) + 'px'; cv.style.touchAction = 'none';
cv.style.borderRadius = '12px'; cv.style.display = 'block';
host.appendChild(cv); return cv;
}
function fit(cv) {
var dpr = W.devicePixelRatio || 1, w = cv.offsetWidth || 280, h = cv.offsetHeight || 200;
cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
var ctx = cv.getContext && cv.getContext('2d'); if (!ctx) return null; // jsdom без canvas
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return { ctx: ctx, W: w, H: h };
}
/* общий движок вращения: state выше redraw, window-listeners */
function attachRotate(cv, state, redraw) {
var dragging = false, lx = 0, ly = 0;
cv.addEventListener('pointerdown', function (e) { dragging = true; lx = e.clientX; ly = e.clientY; state.spin = false; });
W.addEventListener('pointermove', function (e) {
if (!dragging) return;
state.rotY += (e.clientX - lx) * 0.01; state.rotX += (e.clientY - ly) * 0.01;
lx = e.clientX; ly = e.clientY; redraw();
});
W.addEventListener('pointerup', function () { dragging = false; });
}
/* ── 3D-модель молекулы ── */
function molModel(mount, key) {
var host = typeof mount === 'string' ? D.querySelector(mount) : mount;
if (!host || !BIO()) return null;
var keys = Object.keys(MOL);
host.innerHTML = '<div class="fld"><label>Молекула</label><select class="mol-sel">' +
keys.map(function (k) { return '<option value="' + k + '"' + (k === key ? ' selected' : '') + '>' + MOL[k].name + '</option>'; }).join('') + '</select>'
+ '<button class="btn mol-spin">⟳ Вращение</button></div>';
var stage = D.createElement('div'); host.appendChild(stage);
var cv = mkCanvas(stage, 200);
var info = D.createElement('div'); info.className = 'out mol-info'; host.appendChild(info);
var sel = host.querySelector('.mol-sel'), spinBtn = host.querySelector('.mol-spin');
var state = { rotX: -0.35, rotY: 0.6, scale: 2.6, spin: true };
var cur;
function load(k) {
cur = MOL[k]; var g = BIO().vsepr(cur.atoms, cur.bonds); cur.g = g;
var pol = BIO().polarity(cur.atoms, cur.bonds);
var mr = C().molarMass ? C().molarMass(k) : BIO().molarMass(cur.atoms);
var bondTxt = cur.atoms.length === 2 && C().bondClass
? C().bondClass(cur.atoms[0].s, cur.atoms[1].s).type
: (pol.label === 'Ионная' ? 'ионная' : 'ковалентная');
info.className = 'out mol-info ok';
info.innerHTML = '<span class="bd"><b>' + cur.name + '</b> · M = ' + (C().fmt ? C().fmt(mr) : mr) + ' г/моль<br>'
+ 'Связь: ' + bondTxt + ' · молекула: <b>' + pol.label.toLowerCase() + '</b>'
+ (g.shape ? ' · форма: ' + g.shape : '') + '</span>';
}
function redraw() {
var d = fit(cv); if (!d) return;
BIO().render3D(d.ctx, cur.g.atoms3d, cur.bonds, { W: d.W, H: d.H, rotX: state.rotX, rotY: state.rotY, scale: state.scale }, { bg: '#0b1220' });
}
sel.addEventListener('change', function () { load(sel.value); redraw(); });
spinBtn.addEventListener('click', function () { state.spin = !state.spin; spinBtn.classList.toggle('primary', state.spin); });
attachRotate(cv, state, redraw);
load(key && MOL[key] ? key : keys[0]);
redraw();
if (fit(cv)) (function loop() { if (state.spin) { state.rotY += 0.012; redraw(); } W.requestAnimationFrame(loop); })(); // не стартуем цикл без canvas-контекста (jsdom)
return { el: host };
}
/* ── кристаллические решётки (§41) ── */
var LAT = {
ionic: { name: 'Ионная (NaCl)', build: function () { return cube(['Na', 'Cl']); }, note: 'Узлы — ионы Na⁺ и Cl⁻. Прочная решётка → тугоплавкие, твёрдые вещества.' },
atomic: { name: 'Атомная (алмаз)', build: function () { return cube(['C', 'C']); }, note: 'Узлы — атомы, связанные ковалентно. Очень твёрдые, тугоплавкие.' },
molecular: { name: 'Молекулярная (лёд)', build: function () { return cube(['O', 'O']); }, note: 'Узлы — молекулы со слабым притяжением. Летучие, легкоплавкие.' },
metallic: { name: 'Металлическая (Fe)', build: function () { return cube(['Fe', 'Fe'], true); }, note: 'Ион-остовы металла в «электронном газе». Ковкие, проводят ток.' }
};
function cube(symPair, electrons) {
var L = 16, atoms = [], id = 1;
for (var xi = -1; xi <= 1; xi += 2) for (var yi = -1; yi <= 1; yi += 2) for (var zi = -1; zi <= 1; zi += 2) {
var parity = ((xi + yi + zi) / 2 + 3) % 2;
atoms.push({ id: id++, s: symPair[parity], x: xi * L, y: yi * L, z: zi * L });
}
var bonds = [];
for (var i = 0; i < atoms.length; i++) for (var j = i + 1; j < atoms.length; j++) {
var a = atoms[i], b = atoms[j], dd = Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.z - b.z);
if (dd === 2 * L) bonds.push({ f: a.id, t: b.id, o: 1 });
}
if (electrons) for (var e = 0; e < 6; e++) atoms.push({ id: id++, s: 'H', x: (e % 3 - 1) * L, y: ((e / 3 | 0) * 2 - 1) * L * 0.5, z: 0 }); // «электроны» как мелкие точки (H — мелкий радиус)
return { atoms: atoms, bonds: bonds };
}
function latticeViewer(mount, type) {
var host = typeof mount === 'string' ? D.querySelector(mount) : mount;
if (!host || !BIO()) return null;
var keys = Object.keys(LAT);
host.innerHTML = '<div class="fld"><label>Тип решётки</label><select class="lat-sel">' +
keys.map(function (k) { return '<option value="' + k + '"' + (k === type ? ' selected' : '') + '>' + LAT[k].name + '</option>'; }).join('') + '</select></div>';
var stage = D.createElement('div'); host.appendChild(stage);
var cv = mkCanvas(stage, 200);
var info = D.createElement('div'); info.className = 'out'; host.appendChild(info);
var sel = host.querySelector('.lat-sel');
var state = { rotX: -0.4, rotY: 0.5, scale: 2.4, spin: true };
var cur;
function load(k) { var l = LAT[k]; cur = l.build(); info.className = 'out ok'; info.innerHTML = '<span class="bd"><b>' + l.name + '</b><br>' + l.note + '</span>'; }
function redraw() { var d = fit(cv); if (!d) return; BIO().render3D(d.ctx, cur.atoms, cur.bonds, { W: d.W, H: d.H, rotX: state.rotX, rotY: state.rotY, scale: state.scale }, { bg: '#0b1220' }); }
sel.addEventListener('change', function () { load(sel.value); redraw(); });
attachRotate(cv, state, redraw);
load(type && LAT[type] ? type : keys[0]); redraw();
if (fit(cv)) (function loop() { if (state.spin) { state.rotY += 0.01; redraw(); } W.requestAnimationFrame(loop); })();
return { el: host };
}
W.Chem8Mol = { molModel: molModel, latticeViewer: latticeViewer, MOL: MOL };
})(window);