diff --git a/backend/tests/chemistry8-page.test.js b/backend/tests/chemistry8-page.test.js index 0f96f32..2d7ab7f 100644 --- a/backend/tests/chemistry8-page.test.js +++ b/backend/tests/chemistry8-page.test.js @@ -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'); diff --git a/frontend/css/chem8-textbook.css b/frontend/css/chem8-textbook.css index 5d971f9..9fa2a7d 100644 --- a/frontend/css/chem8-textbook.css +++ b/frontend/css/chem8-textbook.css @@ -401,6 +401,11 @@ html.dark .lat-card h4{color:var(--pri-l)} .amph-stage{display:flex;justify-content:center;margin:8px 0} .amph-out{margin-top:6px} +/* 3D-модели молекул/решёток (§38,41) */ +.mol-cv{background:#0b1220;cursor:grab;border:1px solid var(--border)} +.mol-cv:active{cursor:grabbing} +.mol-info{margin-top:8px} + /* геном-карта классов (§22) */ .gm-svg{width:100%;max-width:440px;height:auto;color:var(--text);display:block;margin:4px auto} .gm-out{margin-top:8px} diff --git a/frontend/js/chem8_ch4_widgets.js b/frontend/js/chem8_ch4_widgets.js index 674fb31..013efc4 100644 --- a/frontend/js/chem8_ch4_widgets.js +++ b/frontend/js/chem8_ch4_widgets.js @@ -6,9 +6,14 @@ function C() { return W.Chem8 || {}; } function $(id) { return document.getElementById(id); } + function M() { return W.Chem8Mol; } function mount_p37() { var el = $('c-bond1'); if (el && !el._b && C().bondType) { el._b = 1; C().bondType(el, { a: 'H', b: 'H' }); } } - function mount_p38() { var el = $('c-bond2'); if (el && !el._b && C().bondType) { el._b = 1; C().bondType(el, { a: 'H', b: 'Cl' }); } } + function mount_p38() { + var el = $('c-bond2'); if (el && !el._b && C().bondType) { el._b = 1; C().bondType(el, { a: 'H', b: 'Cl' }); } + var mol = $('c-mol'); if (mol && !mol._b && M()) { mol._b = 1; M().molModel(mol, 'H2O'); } + } + function mount_p41() { var el = $('c-lattice'); if (el && !el._b && M()) { el._b = 1; M().latticeViewer(el, 'ionic'); } } W.CHEM8_WIDGETS = {}; - W.FLAG_MOUNTS = { p37: mount_p37, p38: mount_p38 }; + W.FLAG_MOUNTS = { p37: mount_p37, p38: mount_p38, p41: mount_p41 }; })(window); diff --git a/frontend/js/chem8_mol.js b/frontend/js/chem8_mol.js new file mode 100644 index 0000000..06b842c --- /dev/null +++ b/frontend/js/chem8_mol.js @@ -0,0 +1,132 @@ +/* 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 = '
Если ЭО атомов одинакова (H₂, Cl₂) — общая пара поделена поровну, связь неполярная. Если ЭО разная (HCl, H₂O) — пара смещена к более электроотрицательному атому, связь полярная (возникают частичные заряды δ+ и δ−).
') +flag('Конструктор связи: ΔЭО → тип','Меняй атомы — видно, как разница ЭО превращает связь из неполярной в полярную и далее в ионную.','') +makeCard('lab','Лабораторный опыт 4 · Составление моделей молекул',null,'Соберите шаростержневые модели молекул H₂, Cl₂, HCl, H₂O, CO₂. Определите для каждой тип связи (полярная/неполярная) и форму молекулы. Сравните: в симметричных молекулах (CO₂) полярные связи «компенсируются», и молекула в целом неполярна.
') + +flag('3D-модели молекул','Выбери молекулу и вращай её мышью. Снизу — молярная масса, тип связи, форма и полярность.','') +rememberBox(['ΔЭО ≈ 0 → неполярная; ΔЭО заметна → полярная; ΔЭО велика → ионная.','Более электроотрицательный атом получает δ−.']) +qList(['Полярна ли связь в Cl₂? А в HCl?','Что показывает электроотрицательность?']) +secNav('p37','p39')+readButton('p38'); wireReadBtn('p38'); } @@ -207,6 +209,7 @@ function bp41(){ document.getElementById('p41-body').innerHTML = +'