feat(biochem): Фаза 7 — импорт SMILES + экспорт PNG/JSON

BIO.parseSmiles — парсер учебного подмножества SMILES (органические атомы
верхнего регистра, связи -=#, ветви (), замыкание циклов цифрами/%nn,
неявные H по валентности, 2D-укладка BFS). BIO.toJSON/download.

biochem.html: поле ввода SMILES + кнопка Импорт (Enter), кнопки экспорта
PNG (текущий холст 2D/3D) и JSON.

Проверено: CCO→C2H6O, CC(=O)O→C2H4O2, C1=CC=CC=C1→C6H6 (Кекуле),
ClC(Cl)(Cl)Cl→CCl4, OCC(O)CO→C3H8O3 (глицерин); мусор отсекается.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 13:24:55 +03:00
parent a97896d293
commit af25a845c9
2 changed files with 164 additions and 1 deletions
+48
View File
@@ -435,6 +435,18 @@
<button class="bp-btn bp-btn-secondary" onclick="loadFromLibrary()">
Загрузить из библиотеки
</button>
<!-- SMILES import -->
<div style="display:flex;gap:6px;margin-top:8px">
<input type="text" id="smiles-in" placeholder="SMILES, напр. CCO" spellcheck="false"
onkeydown="if(event.key==='Enter')importSmiles()"
style="flex:1;min-width:0;padding:7px 10px;border-radius:8px;background:rgba(255,255,255,.06);border:1.5px solid rgba(255,255,255,.12);color:#ddd;font:600 .78rem monospace">
<button class="bp-btn bp-btn-secondary" style="width:auto;margin:0;padding:0 14px" onclick="importSmiles()">Импорт</button>
</div>
<!-- Export -->
<div style="display:flex;gap:6px;margin-top:6px">
<button class="bp-btn bp-btn-secondary" style="margin:0;flex:1" onclick="exportPNG()">PNG</button>
<button class="bp-btn bp-btn-secondary" style="margin:0;flex:1" onclick="exportJSON()">JSON</button>
</div>
</div>
<div class="bp-section" id="bp-active-challenge" style="display:none">
<div class="bp-label" id="bp-chal-type-label">Текущее задание</div>
@@ -1260,6 +1272,42 @@ async function saveCurrentMolecule() {
} catch(e) { LS.toast('Ошибка: '+e.message, 'error'); }
}
// ── SMILES import ──
function importSmiles() {
const inp = document.getElementById('smiles-in');
const smi = (inp.value || '').trim();
if (!smi) return;
const parsed = BIO.parseSmiles(smi);
if (!parsed || !parsed.atoms.length) {
LS.toast('Не удалось разобрать SMILES (поддержан верхний регистр: CCO, C1=CC=CC=C1)', 'error');
return;
}
pushHistory();
// переносим в редактор (bonds в формате {from,to,order})
const idMap = {};
atoms = parsed.atoms.map(a => { const nid = nextId++; idMap[a.id] = nid; return { id: nid, s: a.s, x: a.x, y: a.y }; });
bonds = parsed.bonds.map(b => ({ id: nextId++, from: idMap[b.f], to: idMap[b.t], order: b.o }));
inp.value = '';
if (_is3D) _build3D();
centerView(); updateInfo();
LS.toast(`Импортировано: ${BIO.hillFormula(atoms)}`, 'success');
}
// ── Export ──
function exportPNG() {
if (!atoms.length) { LS.toast('Пустой холст', 'info'); return; }
const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = (hillFormula() || 'molecule') + (_is3D ? '-3d' : '') + '.png';
a.click();
}
function exportJSON() {
if (!atoms.length) { LS.toast('Пустой холст', 'info'); return; }
BIO.download((hillFormula() || 'molecule') + '.json',
BIO.toJSON(atoms, bonds.map(b => ({ f: b.from, t: b.to, o: b.order })), hillFormula()),
'application/json');
}
// ── Library ──
async function loadFromLibrary() {
if (!_libAll.length) _libAll = await LS.biochemGetMolecules();