/* * biochem-core.js — общее ядро модуля «Биохимия» (window.BIO) * * Единый источник правды для всех 5 страниц биохимии: * - ELEMENTS: реестр элементов (цвет CPK, масса, валентность, электроотрицательность, * ковалентный и ван-дер-ваальсов радиусы, число валентных электронов) * - формулы/масса: hillFormula, molarMass, parseFormula * - нормализация связей: bF/bT/bO (чинит расхождение полей f/from, t/to, o/order) * - 2D-рендер: render2D (ball-and-stick для превью) * - 3D-геометрия: vsepr (генератор настоящих 3D-координат по теории ОЭПВО/VSEPR) * - 3D-рендер: render3D (ball-and-stick с глубиной и затенением) * - safe: обёртка для API-вызовов с тостом ошибки * - RING_TEMPLATES: шаблоны колец * * Зависимостей нет; LS.toast используется опционально (если доступен). */ (function (global) { 'use strict'; /* ── Реестр элементов ───────────────────────────────────────────────── * color — CPK-цвет заливки * text — цвет символа поверх заливки * radius — радиус кружка в 2D-редакторе (усл. ед.) * mass — атомная масса (г/моль) * maxV — типичная максимальная валентность (для проверки и оценки геометрии) * en — электроотрицательность по Полингу (для полярности связей) * ve — число валентных электронов (для оценки неподелённых пар → VSEPR) * cov — ковалентный радиус, пм (длины связей в 3D) * vdw — ван-дер-ваальсов радиус, пм (space-fill режим) * metal — ионный/металлический центр (нет ковалентной геометрии) */ const ELEMENTS = { H: { name:'Водород', color:'#D4D4D4', text:'#222', radius:18, mass:1.008, maxV:1, en:2.20, ve:1, cov:31, vdw:120 }, C: { name:'Углерод', color:'#555555', text:'#fff', radius:20, mass:12.011, maxV:4, en:2.55, ve:4, cov:76, vdw:170 }, N: { name:'Азот', color:'#4060FF', text:'#fff', radius:20, mass:14.007, maxV:3, en:3.04, ve:5, cov:71, vdw:155 }, O: { name:'Кислород', color:'#EE2020', text:'#fff', radius:20, mass:15.999, maxV:2, en:3.44, ve:6, cov:66, vdw:152 }, P: { name:'Фосфор', color:'#FF8000', text:'#fff', radius:22, mass:30.974, maxV:5, en:2.19, ve:5, cov:107, vdw:180 }, S: { name:'Сера', color:'#C8B400', text:'#000', radius:22, mass:32.06, maxV:6, en:2.58, ve:6, cov:105, vdw:180 }, F: { name:'Фтор', color:'#33CC33', text:'#fff', radius:18, mass:18.998, maxV:1, en:3.98, ve:7, cov:57, vdw:147 }, Cl: { name:'Хлор', color:'#00A860', text:'#fff', radius:22, mass:35.45, maxV:1, en:3.16, ve:7, cov:102, vdw:175 }, Br: { name:'Бром', color:'#A52A2A', text:'#fff', radius:24, mass:79.904, maxV:1, en:2.96, ve:7, cov:120, vdw:185 }, I: { name:'Иод', color:'#940094', text:'#fff', radius:26, mass:126.90, maxV:1, en:2.66, ve:7, cov:139, vdw:198 }, Na: { name:'Натрий', color:'#8040C0', text:'#fff', radius:22, mass:22.990, maxV:1, en:0.93, ve:1, cov:166, vdw:227, metal:true }, K: { name:'Калий', color:'#8F40D4', text:'#fff', radius:24, mass:39.098, maxV:1, en:0.82, ve:1, cov:203, vdw:275, metal:true }, Ca: { name:'Кальций', color:'#707070', text:'#fff', radius:22, mass:40.078, maxV:2, en:1.00, ve:2, cov:176, vdw:231, metal:true }, Mg: { name:'Магний', color:'#1E8A1E', text:'#fff', radius:22, mass:24.305, maxV:2, en:1.31, ve:2, cov:141, vdw:173, metal:true }, Fe: { name:'Железо', color:'#B03010', text:'#fff', radius:22, mass:55.845, maxV:3, en:1.83, ve:8, cov:132, vdw:194, metal:true }, }; function el(sym) { return ELEMENTS[sym] || { name:sym, color:'#888', text:'#fff', radius:20, mass:0, maxV:4, en:2.5, ve:4, cov:75, vdw:170 }; } /* ── Нормализация связей ────────────────────────────────────────────── * В БД связи хранятся как {f,t,o}; в редакторе — как {from,to,order}. * Эти хелперы устраняют расхождение (бывший баг `b.o || b.order`). */ function bF(b) { return b.from != null ? b.from : b.f; } function bT(b) { return b.to != null ? b.to : b.t; } function bO(b) { return (b.order != null ? b.order : b.o) || 1; } /* ── Формулы и масса ──────────────────────────────────────────────────── */ function counts(atoms) { const c = {}; for (const a of atoms) c[a.s] = (c[a.s] || 0) + 1; return c; } function hillFormula(atoms) { if (!atoms || !atoms.length) return ''; const cnt = counts(atoms); const parts = []; if (cnt.C) { parts.push('C' + (cnt.C > 1 ? cnt.C : '')); delete cnt.C; } if (cnt.H) { parts.push('H' + (cnt.H > 1 ? cnt.H : '')); delete cnt.H; } for (const e of Object.keys(cnt).sort()) parts.push(e + (cnt[e] > 1 ? cnt[e] : '')); return parts.join(''); } function molarMass(atoms) { let m = 0; for (const a of atoms) m += el(a.s).mass; return m; } // Разбор строковой формулы (с поддержкой скобок и цифр): "Ca(OH)2" → {Ca:1,O:2,H:2} function parseFormula(str) { const out = {}; const re = /([A-Z][a-z]?)(\d*)|(\()|(\))(\d*)/g; const stack = [out]; let m; while ((m = re.exec(str)) !== null) { if (m[1]) { const n = m[2] ? parseInt(m[2], 10) : 1; const top = stack[stack.length - 1]; top[m[1]] = (top[m[1]] || 0) + n; } else if (m[3]) { stack.push({}); } else if (m[4] !== undefined) { const grp = stack.pop(); const mult = m[5] ? parseInt(m[5], 10) : 1; const top = stack[stack.length - 1]; for (const k in grp) top[k] = (top[k] || 0) + grp[k] * mult; } } return out; } /* ── Степень ненасыщенности (DBE) ─────────────────────────────────────── */ function dbe(atoms) { const c = counts(atoms); const C = c.C || 0, H = c.H || 0, N = c.N || 0, P = c.P || 0; const X = (c.Cl || 0) + (c.F || 0) + (c.Br || 0) + (c.I || 0); if (!C && !H) return null; return (2 * C + 2 + N + P - H - X) / 2; } /* ── 2D-рендер (ball-and-stick для превью) ──────────────────────────────── * atoms: [{s,x,y}] bonds: [{f,t,o}] | [{from,to,order}] * opts: { fit:true|false, padding, bg, lineColor, showSymbols, hideH, scale } * Если fit=true — масштабирует молекулу под размер canvas (для thumbnail). */ function render2D(ctx, atoms, bonds, opts) { opts = opts || {}; const W = ctx.canvas.width, H = ctx.canvas.height; if (opts.bg) { ctx.fillStyle = opts.bg; ctx.fillRect(0, 0, W, H); } else ctx.clearRect(0, 0, W, H); if (!atoms || !atoms.length) return; let sc = opts.scale || 1, ox = W / 2, oy = H / 2; let cx = 0, cy = 0; for (const a of atoms) { cx += a.x; cy += a.y; } cx /= atoms.length; cy /= atoms.length; if (opts.fit !== false) { let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (const a of atoms) { const r = el(a.s).radius; minX = Math.min(minX, a.x - r); maxX = Math.max(maxX, a.x + r); minY = Math.min(minY, a.y - r); maxY = Math.max(maxY, a.y + r); } const pad = opts.padding != null ? opts.padding : 14; const bw = maxX - minX || 1, bh = maxY - minY || 1; sc = Math.min((W - pad * 2) / bw, (H - pad * 2) / bh); if (opts.maxScale) sc = Math.min(sc, opts.maxScale); } const P = (a) => ({ x: (a.x - cx) * sc + ox, y: (a.y - cy) * sc + oy }); const byId = {}; for (const a of atoms) byId[a.id] = a; // Bonds ctx.lineCap = 'round'; for (const b of bonds || []) { const a1 = byId[bF(b)], a2 = byId[bT(b)]; if (!a1 || !a2) continue; const p1 = P(a1), p2 = P(a2); const dx = p2.x - p1.x, dy = p2.y - p1.y, len = Math.hypot(dx, dy) || 1; const px = -dy / len, py = dx / len; const o = bO(b); ctx.strokeStyle = opts.lineColor || '#7a8290'; ctx.lineWidth = Math.max(1, 2.2 * sc); const off = 3 * sc; const seg = (k) => { ctx.beginPath(); ctx.moveTo(p1.x + px * k, p1.y + py * k); ctx.lineTo(p2.x + px * k, p2.y + py * k); ctx.stroke(); }; if (o === 1) seg(0); else if (o === 2) { seg(-off); seg(off); } else { seg(0); seg(-off * 1.4); seg(off * 1.4); } } // Atoms const showSym = opts.showSymbols !== false; for (const a of atoms) { const e = el(a.s); if (opts.hideH && a.s === 'H') continue; const p = P(a); const r = Math.max(3, e.radius * sc * (opts.atomScale || 1)); const grd = ctx.createRadialGradient(p.x - r * 0.3, p.y - r * 0.35, r * 0.1, p.x, p.y, r); grd.addColorStop(0, _lighten(e.color, 90)); grd.addColorStop(0.5, e.color); grd.addColorStop(1, _darken(e.color, 0.55)); ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fillStyle = grd; ctx.fill(); if (showSym && r > 6 && (a.s !== 'H' || r > 9)) { ctx.fillStyle = e.text || '#fff'; ctx.font = `bold ${Math.max(7, Math.round(r * 0.8))}px Manrope, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(a.s, p.x, p.y); } } } /* ── VSEPR: генерация настоящей 3D-геометрии ────────────────────────────── * Вход: atoms [{id,s,x,y}], bonds [{f,t,o}|{from,to,order}] * Выход: { * atoms3d: [{id,s,x,y,z}] — 3D-координаты (усл. ед., ~как 2D-масштаб), * perAtom: { id: {domains, shape, hybridization, lonePairs} }, * shape: строка-описание формы для малых молекул (напр. «угловая»), * angle: характерный валентный угол центральной молекулы (°) | null * } * * Алгоритм: для каждого атома считаем число электронных доменов * (соседи + неподелённые пары) → идеальная геометрия → BFS-укладка в 3D, * ориентируя набор идеальных направлений так, чтобы связь к «родителю» * совпала с одним из направлений. */ const TET = (function () { const k = 1 / Math.sqrt(3); return [[k, k, k], [k, -k, -k], [-k, k, -k], [-k, -k, k]]; })(); const TRIG = [[1, 0, 0], [-0.5, 0.8660254, 0], [-0.5, -0.8660254, 0]]; const LIN = [[1, 0, 0], [-1, 0, 0]]; const OCT = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]]; const TRIGBIPY = [[0, 0, 1], [0, 0, -1], [1, 0, 0], [-0.5, 0.8660254, 0], [-0.5, -0.8660254, 0]]; function _idealDirs(domains) { if (domains <= 1) return [[1, 0, 0]]; if (domains === 2) return LIN; if (domains === 3) return TRIG; if (domains === 4) return TET; if (domains === 5) return TRIGBIPY; return OCT; } function _shapeName(centerSym, neighbors, lonePairs) { const d = neighbors + lonePairs; if (neighbors === 1) return 'двухатомная'; if (d === 2) return 'линейная'; if (d === 3) return lonePairs === 0 ? 'тригональная' : 'угловая'; if (d === 4) { if (lonePairs === 0) return 'тетраэдрическая'; if (lonePairs === 1) return 'пирамидальная'; return 'угловая'; } if (d === 5) return 'тригонально-бипирамидальная'; return 'октаэдрическая'; } function _hyb(domains) { return domains <= 2 ? 'sp' : domains === 3 ? 'sp²' : domains === 4 ? 'sp³' : domains === 5 ? 'sp³d' : 'sp³d²'; } function _idealAngle(domains, lonePairs) { if (domains === 2) return 180; if (domains === 3) return lonePairs ? 117 : 120; if (domains === 4) return lonePairs === 0 ? 109.5 : lonePairs === 1 ? 107 : 104.5; if (domains === 5) return 90; return 90; } // Вращение, переводящее единичный вектор a в единичный вектор b (Родригес) function _rotBetween(a, b) { const v = _cross(a, b); const c = _dot(a, b); const s = _len(v); if (s < 1e-9) { if (c > 0) return [[1,0,0],[0,1,0],[0,0,1]]; // совпадают // противоположны — поворот на 180° вокруг любой перпендикулярной оси const ax = Math.abs(a[0]) < 0.9 ? [1,0,0] : [0,1,0]; const k = _norm(_cross(a, ax)); return _rotAxis(k, Math.PI); } const k = [v[0]/s, v[1]/s, v[2]/s]; return _rotAxis(k, Math.atan2(s, c)); } function _rotAxis(k, ang) { const c = Math.cos(ang), s = Math.sin(ang), t = 1 - c; const [x, y, z] = k; return [ [t*x*x + c, t*x*y - s*z, t*x*z + s*y], [t*x*y + s*z, t*y*y + c, t*y*z - s*x], [t*x*z - s*y, t*y*z + s*x, t*z*z + c], ]; } function _apply(M, v) { return [ M[0][0]*v[0] + M[0][1]*v[1] + M[0][2]*v[2], M[1][0]*v[0] + M[1][1]*v[1] + M[1][2]*v[2], M[2][0]*v[0] + M[2][1]*v[1] + M[2][2]*v[2], ]; } function _cross(a, b) { return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]; } function _dot(a, b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; } function _len(v) { return Math.hypot(v[0], v[1], v[2]); } function _norm(v) { const l = _len(v) || 1; return [v[0]/l, v[1]/l, v[2]/l]; } function vsepr(atoms, bonds) { const idList = atoms.map(a => a.id); const byId = {}; atoms.forEach(a => byId[a.id] = a); // adjacency: id -> [{id, order}] const adj = {}; idList.forEach(i => adj[i] = []); let bondSum = {}; idList.forEach(i => bondSum[i] = 0); for (const b of bonds || []) { const f = bF(b), t = bT(b), o = bO(b); if (adj[f] && adj[t]) { adj[f].push({ id: t, order: o }); adj[t].push({ id: f, order: o }); bondSum[f] += o; bondSum[t] += o; } } // per-atom geometry descriptor const perAtom = {}; for (const a of atoms) { const e = el(a.s); const deg = adj[a.id].length; let lp = 0; if (!e.metal && a.s !== 'H') { lp = Math.max(0, Math.round((e.ve - bondSum[a.id]) / 2)); // у H, галогенов-терминалов и т.п. домены = соседи (терминальные) } const domains = Math.max(deg, deg + lp, 1); perAtom[a.id] = { domains, lonePairs: lp, neighbors: deg, shape: _shapeName(a.s, deg, lp), hybridization: _hyb(domains), angle: _idealAngle(domains, lp), }; } // bond length in canvas units between two atoms const blen = (s1, s2) => (el(s1).cov + el(s2).cov) * 0.62; // C-C ≈ 94 // BFS embedding const pos = {}; // id -> [x,y,z] const placedDirs = {}; // id -> array of used unit directions (from this atom) const visited = new Set(); // root = atom with max degree (центральный) let root = idList[0]; for (const i of idList) if (adj[i].length > adj[root].length) root = i; const queue = []; pos[root] = [0, 0, 0]; placedDirs[root] = []; visited.add(root); queue.push(root); while (queue.length) { const cur = queue.shift(); const e = byId[cur].s; const info = perAtom[cur]; const ideal = _idealDirs(info.domains).map(_norm); // Сколько направлений уже занято связью к родителю const usedDirs = placedDirs[cur].slice(); // Ориентируем идеальный набор так, чтобы ideal[0] совпал с первым занятым let dirs = ideal; if (usedDirs.length) { const R = _rotBetween(ideal[0], usedDirs[0]); dirs = ideal.map(d => _norm(_apply(R, d))); } // помечаем направления, ближайшие к уже занятым, как использованные const taken = new Array(dirs.length).fill(false); for (const u of usedDirs) { let best = -1, bestDot = -2; for (let i = 0; i < dirs.length; i++) { if (taken[i]) continue; const dd = _dot(dirs[i], u); if (dd > bestDot) { bestDot = dd; best = i; } } if (best >= 0) taken[best] = true; } // распределяем оставшиеся направления по непосещённым соседям const freeIdx = []; for (let i = 0; i < dirs.length; i++) if (!taken[i]) freeIdx.push(i); let fi = 0; for (const nb of adj[cur]) { if (visited.has(nb.id)) continue; const dir = dirs[freeIdx[fi++]] || _norm([Math.random()-0.5, Math.random()-0.5, Math.random()-0.5]); const L = blen(e, byId[nb.id].s); pos[nb.id] = [pos[cur][0] + dir[0]*L, pos[cur][1] + dir[1]*L, pos[cur][2] + dir[2]*L]; placedDirs[nb.id] = [[-dir[0], -dir[1], -dir[2]]]; // обратное направление к родителю (placedDirs[cur] = placedDirs[cur] || []).push(dir); visited.add(nb.id); queue.push(nb.id); } } // атомы вне основного компонента (несвязанные) — раскидываем по сетке let stray = 0; for (const a of atoms) { if (!pos[a.id]) { pos[a.id] = [(stray % 4) * 90 - 135, Math.floor(stray / 4) * 90, 0]; stray++; } } // центрируем let cx = 0, cy = 0, cz = 0, n = atoms.length || 1; for (const a of atoms) { cx += pos[a.id][0]; cy += pos[a.id][1]; cz += pos[a.id][2]; } cx /= n; cy /= n; cz /= n; const atoms3d = atoms.map(a => ({ id: a.id, s: a.s, x: pos[a.id][0] - cx, y: pos[a.id][1] - cy, z: pos[a.id][2] - cz, })); // характеристика молекулы в целом (по центральному тяжёлому атому) let centerId = root; for (const a of atoms) if (a.s !== 'H' && perAtom[a.id].neighbors > perAtom[centerId].neighbors) centerId = a.id; const ci = perAtom[centerId]; return { atoms3d, perAtom, shape: ci ? ci.shape : null, angle: ci && ci.neighbors >= 2 ? ci.angle : null, hybridization: ci ? ci.hybridization : null, centerSym: byId[centerId] ? byId[centerId].s : null, }; } /* ── 3D-рендер (ball-and-stick с глубиной) ──────────────────────────────── * atoms3d: [{id,s,x,y,z}] bonds: нормализуются * cam: { rotX, rotY, scale, W, H } * opts: { vdw:false, bg:'#07070f', showSymbols:true } */ function render3D(ctx, atoms3d, bonds, cam, opts) { opts = opts || {}; const W = cam.W, H = cam.H; const cxr = Math.cos(cam.rotX), sxr = Math.sin(cam.rotX); const cyr = Math.cos(cam.rotY), syr = Math.sin(cam.rotY); const fov = 900, sc = cam.scale || 1; if (opts.bg !== null) { ctx.fillStyle = opts.bg || '#07070f'; ctx.fillRect(0, 0, W, H); } if (!atoms3d || !atoms3d.length) return; const proj = atoms3d.map(a => { const x = a.x * sc, y = a.y * sc, z = a.z * sc; const x1 = x * cyr + z * syr; const z1 = -x * syr + z * cyr; const y2 = y * cxr - z1 * sxr; const z2 = y * sxr + z1 * cxr; const persp = fov / (fov + z2); return { a, sx: x1 * persp + W / 2, sy: y2 * persp + H / 2, sz: z2, persp }; }); const pm = {}; for (const p of proj) pm[p.a.id] = p; proj.sort((p, q) => p.sz - q.sz); // дальние раньше (painter) const vdw = !!opts.vdw; if (!vdw) { // связи рисуем в порядке глубины вместе с атомами — упрощённо рисуем все связи, // затем атомы поверх (сортированные). Для корректной глубины интерполируем z. for (const b of bonds || []) { const p1 = pm[bF(b)], p2 = pm[bT(b)]; if (!p1 || !p2) continue; const avg = (p1.persp + p2.persp) / 2; const o = bO(b); const dx = p2.sx - p1.sx, dy = p2.sy - p1.sy, len = Math.hypot(dx, dy) || 1; const ox = -dy / len, oy = dx / len; ctx.strokeStyle = `rgba(190,195,210,${0.30 + avg * 0.55})`; ctx.lineWidth = Math.max(1.4, 4 * avg); ctx.lineCap = 'round'; const seg = (k) => { ctx.beginPath(); ctx.moveTo(p1.sx + ox*k, p1.sy + oy*k); ctx.lineTo(p2.sx + ox*k, p2.sy + oy*k); ctx.stroke(); }; if (o === 1) seg(0); else { const off = 3.2 * avg; for (let i = -(o-1); i <= (o-1); i += 2) seg(off * i); } } } for (const p of proj) { const { a, sx, sy, persp } = p; const e = el(a.s); const baseR = vdw ? (e.vdw / 100) * 16 : (e.cov / 100) * 16 + 5; const r = Math.max(3, baseR * persp * sc * (vdw ? 1.0 : 0.9)); const [r0, g0, b0] = _hexRgb(e.color); 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+115)},${Math.min(255,g0+115)},${Math.min(255,b0+115)})`); grd.addColorStop(0.42, e.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(); if (opts.showSymbols !== false && !vdw && (a.s !== 'H' || r > 12)) { ctx.fillStyle = e.text || '#fff'; ctx.font = `bold ${Math.max(8, Math.round(r * 0.72))}px Manrope, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(a.s, sx, sy); } } } /* ── Цветовые утилиты ─────────────────────────────────────────────────── */ function _hexRgb(hex) { hex = String(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 _lighten(hex, amt) { const [r, g, b] = _hexRgb(hex); return `rgb(${Math.min(255, r + amt)},${Math.min(255, g + amt)},${Math.min(255, b + amt)})`; } function _darken(hex, f) { const [r, g, b] = _hexRgb(hex); return `rgb(${Math.round(r * f)},${Math.round(g * f)},${Math.round(b * f)})`; } /* ── safe: обёртка для API с тостом ошибки ─────────────────────────────── * await BIO.safe(LS.biochemGetMolecules(), 'Не удалось загрузить молекулы') */ async function safe(promiseOrFn, errMsg) { try { return await (typeof promiseOrFn === 'function' ? promiseOrFn() : promiseOrFn); } catch (e) { const msg = errMsg || ('Ошибка: ' + (e && e.message ? e.message : e)); if (global.LS && typeof global.LS.toast === 'function') global.LS.toast(msg, 'error'); else console.error(msg, e); return null; } } /* ── Шаблоны колец (2D-координаты) ───────────────────────────────────── */ const RING_TEMPLATES = { benzene: { atoms: [{s:'C',x:0,y:-55},{s:'C',x:47.6,y:-27.5},{s:'C',x:47.6,y:27.5}, {s:'C',x:0,y:55},{s:'C',x:-47.6,y:27.5},{s:'C',x:-47.6,y:-27.5}], bonds: [[0,1,2],[1,2,1],[2,3,2],[3,4,1],[4,5,2],[5,0,1]], }, cyclohexane: { atoms: [{s:'C',x:0,y:-55},{s:'C',x:47.6,y:-27.5},{s:'C',x:47.6,y:27.5}, {s:'C',x:0,y:55},{s:'C',x:-47.6,y:27.5},{s:'C',x:-47.6,y:-27.5}], bonds: [[0,1,1],[1,2,1],[2,3,1],[3,4,1],[4,5,1],[5,0,1]], }, cyclopentane: { atoms: [{s:'C',x:0,y:-50},{s:'C',x:47.6,y:-15.5},{s:'C',x:29.4,y:40.5}, {s:'C',x:-29.4,y:40.5},{s:'C',x:-47.6,y:-15.5}], bonds: [[0,1,1],[1,2,1],[2,3,1],[3,4,1],[4,0,1]], }, naphthalene: { atoms: [ {s:'C',x:0,y:27.5},{s:'C',x:0,y:-27.5}, {s:'C',x:-47.6,y:-55},{s:'C',x:-95.2,y:-27.5},{s:'C',x:-95.2,y:27.5},{s:'C',x:-47.6,y:55}, {s:'C',x:47.6,y:-55},{s:'C',x:95.2,y:-27.5},{s:'C',x:95.2,y:27.5},{s:'C',x:47.6,y:55}, ], bonds: [ [0,1,1],[1,2,2],[2,3,1],[3,4,2],[4,5,1],[5,0,2], [1,6,2],[6,7,1],[7,8,2],[8,9,1],[9,0,2], ], }, }; /* ── Экспорт ──────────────────────────────────────────────────────────── */ global.BIO = { ELEMENTS, el, bF, bT, bO, counts, hillFormula, molarMass, parseFormula, dbe, render2D, vsepr, render3D, safe, RING_TEMPLATES, _hexRgb, _lighten, _darken, }; })(window);