feat(biochem): ядро biochem-core.js + настоящая 3D-геометрия (VSEPR)
Фаза 0 (фундамент) + Фаза 1 (3D) плана BIOCHEM_UPGRADE: - Новый общий модуль frontend/js/biochem-core.js (window.BIO): реестр элементов (CPK, масса, валентность, электроотрицательность, ковалентный/ ван-дер-ваальсов радиусы), hillFormula/molarMass/parseFormula/dbe, нормализация связей (bF/bT/bO — чинит расхождение полей f/from, o/order), render2D, vsepr (генератор 3D по ОЭПВО), render3D (ball-and-stick с глубиной и затенением), safe (обёртка API с тостом), RING_TEMPLATES. - biochem.html: подключён core; фейковый 3D (плоская проекция a.z||0) заменён на честную VSEPR-геометрию через BIO.render3D; в панель свойств добавлены форма молекулы, гибридизация и валентный угол; фикс бага порядка связи в getBondSum. VSEPR проверен: вода — угловая, метан — тетраэдр 109.5°, CO2 — линейная 180°, NH3 — пирамидальная; sp/sp2/sp3 верно. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-99
@@ -507,6 +507,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/biochem-core.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
<script>
|
||||
@@ -666,7 +667,7 @@ function hillFormula() {
|
||||
return parts.join('');
|
||||
}
|
||||
function getBondSum(id) {
|
||||
return bonds.reduce((s,b) => s + (b.from===id||b.to===id ? b.o || b.order : 0), 0);
|
||||
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 => {
|
||||
@@ -712,6 +713,15 @@ function calcMolStats() {
|
||||
}
|
||||
chips.push(_chip('Полярность', polarity.label, polarity.cls));
|
||||
chips.push(_chip('Атомов', atoms.length, ''));
|
||||
|
||||
// Geometry (VSEPR) — for the central atom
|
||||
if (window.BIO && bonds.length) {
|
||||
const geom = BIO.vsepr(atoms, bonds);
|
||||
if (geom.shape) chips.push(_chip('Геометрия', geom.shape, 'violet'));
|
||||
if (geom.hybridization) chips.push(_chip('Гибридизация', geom.hybridization, 'cyan'));
|
||||
if (geom.angle != null) chips.push(_chip('Угол связи', geom.angle + '°', ''));
|
||||
}
|
||||
|
||||
if (molClass) chips.push(
|
||||
`<div class="bp-stat-chip" style="grid-column:1/-1"><span class="bp-stat-lbl">Класс</span>` +
|
||||
`<span class="bp-stat-val violet">${molClass}</span></div>`
|
||||
@@ -1283,6 +1293,7 @@ function updateInfo() {
|
||||
} else { info.style.display = 'none'; }
|
||||
|
||||
calcMolStats();
|
||||
if (_is3D) _build3D();
|
||||
}
|
||||
|
||||
// ── Build element palette ──
|
||||
@@ -1667,9 +1678,20 @@ let _3dVelY = 0;
|
||||
let _3dSpin = true; // auto-spin flag
|
||||
let _3dSpinTmo = null; // resume-spin timeout
|
||||
|
||||
// VDW radii (van der Waals, canvas units)
|
||||
const VDW_R = { H:38, C:56, N:52, O:50, S:72, P:70, F:40, Cl:72, Br:85,
|
||||
Na:80, Ca:86, K:92, Mg:72, Fe:70 };
|
||||
// Real 3D geometry (VSEPR), rebuilt whenever the molecule changes in 3D mode
|
||||
let _atoms3d = [];
|
||||
function _build3D() {
|
||||
if (window.BIO) _atoms3d = BIO.vsepr(atoms, bonds).atoms3d;
|
||||
else _atoms3d = atoms.map(a => ({ id:a.id, s:a.s, x:a.x, y:a.y, z:0 }));
|
||||
}
|
||||
// Fit scale to the 3D molecule extent so it fills the view nicely
|
||||
function _fit3D() {
|
||||
if (!_atoms3d.length) { scale = 1; return; }
|
||||
let ext = 1;
|
||||
for (const a of _atoms3d) ext = Math.max(ext, Math.hypot(a.x, a.y, a.z));
|
||||
const view = Math.min(canvas.width, canvas.height) * 0.40;
|
||||
scale = Math.max(0.3, Math.min(4, view / (ext * 1.6 + 30)));
|
||||
}
|
||||
|
||||
function toggle3D() {
|
||||
_is3D = !_is3D;
|
||||
@@ -1679,7 +1701,8 @@ function toggle3D() {
|
||||
if (!_is3D && _isVDW) { _isVDW = false; vdwBtn.classList.remove('mode-3d-active'); }
|
||||
if (_is3D) {
|
||||
canvas.style.cursor = 'grab';
|
||||
centerView();
|
||||
_build3D();
|
||||
_fit3D();
|
||||
_start3D();
|
||||
} else {
|
||||
_stop3D();
|
||||
@@ -1719,102 +1742,13 @@ function _stop3D() {
|
||||
if (_3dAnimId) { cancelAnimationFrame(_3dAnimId); _3dAnimId = null; }
|
||||
}
|
||||
|
||||
function _proj3D(x, y, z) {
|
||||
const cy = Math.cos(_3dRotY), sy = Math.sin(_3dRotY);
|
||||
const x1 = x * cy + z * sy;
|
||||
const z1 = -x * sy + z * cy;
|
||||
const cx = Math.cos(_3dRotX), sx2 = Math.sin(_3dRotX);
|
||||
const y2 = y * cx - z1 * sx2;
|
||||
const z2 = y * sx2 + z1 * cx;
|
||||
const fov = 800;
|
||||
const sc = fov / (fov + z2);
|
||||
return { sx: x1 * sc, sy: y2 * sc, sz: z2, sc };
|
||||
}
|
||||
|
||||
function _hexRgb(hex) {
|
||||
hex = hex.replace('#','');
|
||||
if (hex.length === 3) hex = hex.split('').map(c=>c+c).join('');
|
||||
const n = parseInt(hex,16);
|
||||
return [(n>>16)&255,(n>>8)&255,n&255];
|
||||
}
|
||||
|
||||
function render3D() {
|
||||
const W = canvas.width, H = canvas.height;
|
||||
ctx.clearRect(0,0,W,H);
|
||||
ctx.fillStyle = '#07070f';
|
||||
ctx.fillRect(0,0,W,H);
|
||||
|
||||
if (!atoms.length) return;
|
||||
|
||||
// molecule center (world coords)
|
||||
let cx=0, cy=0;
|
||||
for (const a of atoms) { cx+=a.x; cy+=a.y; }
|
||||
cx/=atoms.length; cy/=atoms.length;
|
||||
|
||||
const s3 = scale * 1.15;
|
||||
|
||||
// project all atoms
|
||||
const proj = atoms.map(a => {
|
||||
const p = _proj3D((a.x-cx)*s3, (a.y-cy)*s3, (a.z||0)*s3);
|
||||
return { a, sx: p.sx+W/2, sy: p.sy+H/2, sz: p.sz, sc: p.sc };
|
||||
});
|
||||
proj.sort((a,b) => b.sz - a.sz); // back<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>front
|
||||
|
||||
const pm = {};
|
||||
for (const p of proj) pm[p.a.id] = p;
|
||||
|
||||
// bonds (hidden in VDW mode)
|
||||
if (!_isVDW) {
|
||||
for (const b of bonds) {
|
||||
const p1 = pm[b.from], p2 = pm[b.to];
|
||||
if (!p1||!p2) continue;
|
||||
const avgSc = (p1.sc+p2.sc)/2;
|
||||
ctx.strokeStyle = `rgba(180,180,200,${0.35+avgSc*0.55})`;
|
||||
ctx.lineWidth = Math.max(1.2, 3.5*avgSc);
|
||||
ctx.lineCap = 'round';
|
||||
const order = b.order||1;
|
||||
if (order===1) {
|
||||
ctx.beginPath(); ctx.moveTo(p1.sx,p1.sy); ctx.lineTo(p2.sx,p2.sy); ctx.stroke();
|
||||
} else {
|
||||
const dx=p2.sx-p1.sx, dy=p2.sy-p1.sy, len=Math.hypot(dx,dy)||1;
|
||||
const ox=-dy/len*3.5*avgSc, oy=dx/len*3.5*avgSc;
|
||||
for (let o=-(order-1); o<=(order-1); o+=2) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.sx+ox*o, p1.sy+oy*o);
|
||||
ctx.lineTo(p2.sx+ox*o, p2.sy+oy*o);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// atoms (back<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>front already)
|
||||
for (const p of proj) {
|
||||
const { a, sx, sy, sc } = p;
|
||||
const el = ELEMENTS[a.s]||{color:'#888',text:'#fff',radius:20};
|
||||
const r = _isVDW
|
||||
? Math.max(4, (VDW_R[a.s] || 56) * sc * 0.85)
|
||||
: Math.max(3, el.radius * sc * 1.25);
|
||||
const [r0,g0,b0] = _hexRgb(el.color);
|
||||
// sphere gradient: highlight top-left, dark bottom-right
|
||||
const grd = ctx.createRadialGradient(sx-r*0.32,sy-r*0.38,r*0.06, sx,sy,r);
|
||||
grd.addColorStop(0, `rgb(${Math.min(255,r0+110)},${Math.min(255,g0+110)},${Math.min(255,b0+110)})`);
|
||||
grd.addColorStop(0.42, el.color);
|
||||
grd.addColorStop(1, `rgb(${Math.round(r0*0.2)},${Math.round(g0*0.2)},${Math.round(b0*0.2)})`);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, r, 0, Math.PI*2);
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fill();
|
||||
// label (hide H when tiny, hidden in VDW mode)
|
||||
if (!_isVDW && (a.s !== 'H' || r > 13)) {
|
||||
ctx.fillStyle = el.text||'#fff';
|
||||
ctx.font = `bold ${Math.max(8,Math.round(r*0.72))}px Manrope,sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillText(a.s, sx, sy);
|
||||
}
|
||||
}
|
||||
if (!_atoms3d.length && atoms.length) _build3D();
|
||||
// 3D coords are in canvas units (~real geometry); scale to fit the view
|
||||
BIO.render3D(ctx, _atoms3d, bonds, {
|
||||
rotX: _3dRotX, rotY: _3dRotY, scale: scale * 1.6, W, H,
|
||||
}, { vdw: _isVDW, bg: '#07070f' });
|
||||
}
|
||||
|
||||
// ── Init ──
|
||||
|
||||
Reference in New Issue
Block a user