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>
@
This commit is contained in:
Maxim Dolgolyov
2026-05-30 16:34:37 +03:00
parent dead984d8a
commit 7bf15d449a
5 changed files with 160 additions and 2 deletions
+13
View File
@@ -19,6 +19,7 @@ function buildPage(file, widgetsSrc) {
const inl = {
'/js/biochem-core.js': readF('frontend/js/biochem-core.js'),
'/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
'/js/chem8_mol.js': readF('frontend/js/chem8_mol.js'),
[widgetsSrc]: readF('frontend/js' + widgetsSrc.replace('/js', '')),
'/js/chem8_engine.js': readF('frontend/js/chem8_engine.js')
};
@@ -136,6 +137,18 @@ test('ch4: SPA без ошибок, 7 карточек, §36 активен, т
assert.ok(doc.querySelector('#c-bond2 .bt-out'), 'виджет полярности §38');
});
test('ch4: 3D-модели молекул §38 и решётки §41 монтируются (U4)', async () => {
const { doc, errors } = await loadDom('chemistry_8_ch4.html', '/js/chem8_ch4_widgets.js');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
doc.defaultView.goTo('p38'); await wait(140);
assert.ok(doc.querySelector('#c-mol .mol-sel'), 'выбор молекулы §38');
assert.ok(doc.querySelector('#c-mol canvas'), 'canvas 3D-модели §38');
assert.ok(doc.querySelector('#c-mol .mol-info'), 'инфо о молекуле §38');
doc.defaultView.goTo('p41'); await wait(140);
assert.ok(doc.querySelector('#c-lattice .lat-sel'), 'выбор решётки §41');
assert.ok(doc.querySelector('#c-lattice canvas'), 'canvas решётки §41');
});
/* ── Глава 5 ── */
test('ch5: SPA без ошибок, 5 карточек, §42 активен, с.о. и баланс', async () => {
const { doc, errors } = await loadDom('chemistry_8_ch5.html', '/js/chem8_ch5_widgets.js');