feat(biochem): Фаза 2.1/2.2/2.4 — серверный chem.js + /analyze + подсказки валентности

- 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) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 22:37:59 +03:00
parent 8b5d9238b5
commit b67fac6407
7 changed files with 125 additions and 26 deletions
+18 -5
View File
@@ -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,
+1
View File
@@ -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);
+46
View File
@@ -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 };