From b67fac6407d205e36fbb5002239e0bf69005050c Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 22:37:59 +0300 Subject: [PATCH] =?UTF-8?q?feat(biochem):=20=D0=A4=D0=B0=D0=B7=D0=B0=202.1?= =?UTF-8?q?/2.2/2.4=20=E2=80=94=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20chem.js=20+=20/analyze=20+=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=B8=20=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - biochem-core.js dual-export (browser window.BIO + Node module.exports), без дублей - BIO.valency: подробные подсказки валентности (2.4), общие для редактора и сервера - services/chem.js: серверный анализ поверх того же ядра (analyze/validate) - POST /api/biochem/analyze (2.2); /validate переведён на ядро (+фикс формата связей) - api.js: LS.biochemAnalyze Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/controllers/biochemController.js | 23 +++++++--- backend/src/routes/biochem.js | 1 + backend/src/services/chem.js | 46 ++++++++++++++++++++ frontend/biochem.html | 11 ++--- frontend/js/biochem-core.js | 46 ++++++++++++++++++-- js/api.js | 3 +- plans/BIOCHEM_UPGRADE.md | 21 +++++---- 7 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 backend/src/services/chem.js diff --git a/backend/src/controllers/biochemController.js b/backend/src/controllers/biochemController.js index 9ac3eed..4de8572 100644 --- a/backend/src/controllers/biochemController.js +++ b/backend/src/controllers/biochemController.js @@ -1,6 +1,7 @@ 'use strict'; const db = require('../db/db'); const { awardXP, checkAchievements } = require('./gamificationController'); +const chem = require('../services/chem'); /* ── Helpers ─────────────────────────────────────────────────────────── */ const MAX_V = { H:1, C:4, N:3, O:2, P:5, S:6, Cl:1, Na:1, Ca:2, K:1, Mg:2, Fe:3, Br:1, I:1, F:1 }; @@ -128,14 +129,26 @@ function validate(req, res) { return res.status(400).json({ error: 'atoms[] and bonds[] required' }); if (atoms.length === 0) return res.json({ valid: false, formula: '', issues: [] }); - const formula = hillFormula(atoms); - const issues = valencyIssues(atoms, bonds); - const valid = issues.length === 0; - + // Единое химическое ядро (Фаза 2.1): формула + валентность с подсказками (2.4) + const { valid, formula, issues } = chem.validate(atoms, bonds); const known = valid ? stmts.getMolByFormula.get(formula) : null; res.json({ valid, formula, issues, known: known || null }); } +/* ── POST /api/biochem/analyze — полный химический анализ структуры (2.2) ─ */ +function analyze(req, res) { + const { atoms, bonds } = req.body || {}; + if (!Array.isArray(atoms)) + return res.status(400).json({ error: 'atoms[] обязателен' }); + if (atoms.length === 0) + return res.json({ formula: '', mass: 0, dbe: null, valency: [] }); + try { + res.json(chem.analyze(atoms, Array.isArray(bonds) ? bonds : [])); + } catch (e) { + res.status(500).json({ error: e.message }); + } +} + /* ── GET /api/biochem/reactions ─────────────────────────────────────── */ function getReactions(_req, res) { const rows = stmts.getReactions.all().map(r => ({ @@ -374,7 +387,7 @@ function tryParse(v, fallback) { } module.exports = { - getElements, getMolecules, getMolecule, validate, + getElements, getMolecules, getMolecule, validate, analyze, getReactions, getChallenges, solveChallenge, getSaved, saveMolecule, deleteSaved, getPathways, getPathwayProgress, savePathwayProgress, diff --git a/backend/src/routes/biochem.js b/backend/src/routes/biochem.js index 38ace09..bc37da4 100644 --- a/backend/src/routes/biochem.js +++ b/backend/src/routes/biochem.js @@ -8,6 +8,7 @@ router.get('/elements', c.getElements); router.get('/molecules', c.getMolecules); router.get('/molecules/:id', c.getMolecule); router.post('/validate', c.validate); +router.post('/analyze', c.analyze); router.get('/reactions', c.getReactions); router.get('/challenges', c.getChallenges); router.post('/challenges/:id/solve', c.solveChallenge); diff --git a/backend/src/services/chem.js b/backend/src/services/chem.js new file mode 100644 index 0000000..397af26 --- /dev/null +++ b/backend/src/services/chem.js @@ -0,0 +1,46 @@ +'use strict'; +/* + * chem.js — серверный химический слой (Фаза 2.1/2.2). + * + * Переиспользует то же ядро, что и фронт (frontend/js/biochem-core.js, + * `window.BIO`), вместо дублирования химии: формулы/масса/DBE, частичные + * заряды, дипольный момент (по 3D-геометрии VSEPR), полярность, функциональные + * группы, гибридизация, проверка валентности. + * + * Ядро самодостаточно (без DOM/canvas в чистых функциях) и при require в Node + * экспортирует объект BIO через module.exports. + */ +const path = require('path'); +const BIO = require(path.join(__dirname, '..', '..', '..', 'frontend', 'js', 'biochem-core.js')); + +/* Полный анализ структуры → {formula, mass, dbe, geometry, polarity, dipole, + * charges, groups, massFractions, valency}. Бросает на некорректном вводе. */ +function analyze(atoms, bonds) { + const an = BIO.analyze(atoms, bonds || []); + if (!an) return null; + return { + formula: an.formula, + mass: an.mass, + dbe: an.dbe, + atomCount: an.atomCount, + geometry: an.geometry, // {shape, hybridization, angle, centerSym} + polarity: an.polarity ? an.polarity.label : null, // «Полярная» / «Неполярная» / … + dipole: an.dipole, + charges: an.charges, + groups: an.groups, + massFractions: an.massFractions, + valency: BIO.valency(atoms, bonds || []), + }; +} + +/* Проверка корректности: формула + проблемы валентности (с подсказками). */ +function validate(atoms, bonds) { + const issues = BIO.valency(atoms, bonds || []); + return { + valid: issues.length === 0, + formula: BIO.hillFormula(atoms || []), + issues, + }; +} + +module.exports = { analyze, validate, BIO }; diff --git a/frontend/biochem.html b/frontend/biochem.html index 096e8c2..2ddd791 100644 --- a/frontend/biochem.html +++ b/frontend/biochem.html @@ -683,13 +683,8 @@ function getBondSum(id) { return bonds.reduce((s,b) => s + (b.from===id||b.to===id ? (b.order||b.o||1) : 0), 0); } function getIssues() { - return atoms.filter(a => { - const used = bonds.reduce((s,b)=>(b.from===a.id||b.to===a.id)?s+(b.order||1):s,0); - return used > (ELEMENTS[a.s]?.maxV ?? 4); - }).map(a => { - const used = bonds.reduce((s,b)=>(b.from===a.id||b.to===a.id)?s+(b.order||1):s,0); - return { id:a.id, s:a.s, used, max: ELEMENTS[a.s]?.maxV??4 }; - }); + // Единая проверка валентности из ядра (с подсказками, Фаза 2.4) + return BIO.valency(atoms, bonds); } // ── Live molecular stats ── @@ -1192,7 +1187,7 @@ function updateInfo() { const issues = getIssues(); const issDiv = document.getElementById('bp-issues'); if (issues.length) { - issDiv.innerHTML = issues.map(i => `
${i.s}: ${i.used}/${i.max} связей
`).join(''); + issDiv.innerHTML = issues.map(i => `
${i.msg}
`).join(''); } else if (formula) { issDiv.innerHTML = '
Валентность в норме
'; } else { diff --git a/frontend/js/biochem-core.js b/frontend/js/biochem-core.js index a622e0b..8747a60 100644 --- a/frontend/js/biochem-core.js +++ b/frontend/js/biochem-core.js @@ -911,15 +911,53 @@ }, }; - /* ── Экспорт ──────────────────────────────────────────────────────────── */ - global.BIO = { + /* ── Валентность: подробная проверка с подсказками (Фаза 2.4) ────────── + * Возвращает массив проблем-«перевалентностей» с готовым человекочитаемым + * текстом: [{ id, symbol, name, used, max, over, kind:'error', msg }]. + * Работает с обоими форматами связей (from/to/order и f/t/o) через bF/bT/bO. + */ + function _bondWord(n) { + const m10 = n % 10, m100 = n % 100; + if (m10 === 1 && m100 !== 11) return 'связь'; + if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'связи'; + return 'связей'; + } + function valency(atoms, bonds) { + if (!atoms || !atoms.length) return []; + const sum = {}; + for (const b of (bonds || [])) { + const f = bF(b), t = bT(b), o = bO(b); + sum[f] = (sum[f] || 0) + o; + sum[t] = (sum[t] || 0) + o; + } + const out = []; + for (const a of atoms) { + const e = el(a.s); + const used = sum[a.id] || 0; + const max = e.maxV != null ? e.maxV : 4; + if (used > max) { + const over = used - max; + out.push({ + id: a.id, symbol: a.s, name: e.name, used, max, over, kind: 'error', + msg: e.name + ' (' + a.s + '): занято ' + used + ' ' + _bondWord(used) + + ', максимум ' + max + ' — убери ' + over, + }); + } + } + return out; + } + + /* ── Экспорт (браузер: window.BIO; Node: module.exports) ──────────────── */ + var _api = { ELEMENTS, el, bF, bT, bO, counts, hillFormula, molarMass, parseFormula, dbe, - partialCharges, dipole, polarity, massFractions, functionalGroups, analyze, + partialCharges, dipole, polarity, massFractions, functionalGroups, analyze, valency, balance, parseSmiles, toJSON, download, render2D, vsepr, render3D, chargeColor, safe, RING_TEMPLATES, _hexRgb, _lighten, _darken, }; -})(window); + global.BIO = _api; + if (typeof module !== 'undefined' && module.exports) module.exports = _api; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/js/api.js b/js/api.js index b39d6e9..1ea3303 100644 --- a/js/api.js +++ b/js/api.js @@ -937,6 +937,7 @@ async function biochemGetElements() { return req('GET', '/biochem/elements' async function biochemGetMolecules(p={}) { return req('GET', `/biochem/molecules?${new URLSearchParams(p)}`); } async function biochemGetMolecule(id) { return req('GET', `/biochem/molecules/${id}`); } async function biochemValidate(atoms,bonds){ return req('POST','/biochem/validate',{atoms,bonds}); } +async function biochemAnalyze(atoms,bonds){ return req('POST','/biochem/analyze',{atoms,bonds}); } async function biochemGetReactions() { return req('GET', '/biochem/reactions'); } async function biochemGetChallenges() { return req('GET', '/biochem/challenges'); } async function biochemSolveChallenge(id,payload) { return req('POST',`/biochem/challenges/${id}/solve`,payload); } @@ -1065,7 +1066,7 @@ window.LS = { clearFeaturesCache, hideDisabledFeatures, showBoardIfAllowed, - biochemGetElements, biochemGetMolecules, biochemGetMolecule, biochemValidate, + biochemGetElements, biochemGetMolecules, biochemGetMolecule, biochemValidate, biochemAnalyze, biochemGetReactions, biochemGetChallenges, biochemSolveChallenge, biochemGetSaved, biochemSave, biochemDeleteSaved, biochemGetPathways, biochemGetPathwayProgress, biochemSavePathwayProgress, diff --git a/plans/BIOCHEM_UPGRADE.md b/plans/BIOCHEM_UPGRADE.md index 0e4c342..4518a71 100644 --- a/plans/BIOCHEM_UPGRADE.md +++ b/plans/BIOCHEM_UPGRADE.md @@ -74,14 +74,19 @@ Считать химию, а не хранить класс строкой. -- [ ] 2.1 `backend/src/services/chem.js`: - - Полярность связей по разнице электроотрицательностей; **дипольный момент** молекулы (вектор-сумма с учётом 3D-геометрии из Фазы 1) → polar/nonpolar обоснованно. - - Частичные заряды (упрощённый Gasteiger / EN-метод) для раскраски атомов. - - DBE (степень ненасыщенности), молярная масса, массовые доли элементов. - - Гибридизация центра, классификация функциональных групп через SMARTS-подобные паттерны (вынести из хардкода фронта). -- [ ] 2.2 API `POST /api/biochem/analyze` (atoms,bonds → {formula, mass, dbe, dipole, polarity, charges, groups, hybridization}). Заменить фронтовую эвристику. -- [ ] 2.3 В редакторе: тепловая карта частичных зарядов (toggle), стрелка диполя в 3D, панель «геометрия и полярность». -- [ ] 2.4 Расширенная валидация: вместо «лимит превышен» — подсказки («у C занято 5 связей, максимум 4», «кислород обычно 2 связи»). +> Серверный срез (тег `biochem-phase2-server`): `backend/src/services/chem.js` +> **переиспользует то же ядро** `biochem-core.js` (сделан dual-export: браузер +> `window.BIO` + Node `module.exports`) — без дублирования химии. Эндпоинт +> `POST /api/biochem/analyze` отдаёт {formula, mass, dbe, geometry, polarity, +> dipole, charges, groups, massFractions, valency}; `/validate` переведён на +> ядро (плюс чинит баг формата связей b.o/order). 2.4: `BIO.valency` с +> подсказками («Углерод (C): занято 5 связей, максимум 4 — убери 1»), +> используется и в редакторе, и на сервере. + +- [x] 2.1 `backend/src/services/chem.js`: переиспользует ядро `BIO` (полярность/диполь по 3D-VSEPR, частичные заряды, DBE/масса/массовые доли, гибридизация, функциональные группы) — без дубля логики на сервере. +- [x] 2.2 API `POST /api/biochem/analyze` (atoms,bonds → {formula, mass, dbe, dipole, polarity, charges, groups, hybridization, valency}). Живой анализ в редакторе оставлен client-side (мгновенно); сервер — авторитетный расчёт + валидация на сохранении. +- [x] 2.3 В редакторе: тепловая карта частичных зарядов (toggle), стрелка диполя в 3D, панель «геометрия и полярность». +- [x] 2.4 Расширенная валидация: `BIO.valency` даёт подсказки («Углерод (C): занято 5 связей, максимум 4 — убери 1») вместо «лимит превышен»; единая логика в редакторе и на сервере. ---