Skip to content

Commit 9e99eb8

Browse files
Per-card 2D/3D toggle on library + substitution cards (fix corner render) (#92)
Replace the global browse 2D/3D toggle with a per-card toggle button on each library cell AND each substitution card. The 3D conformer now renders IN the card: a shared mount3D() gives the container position:relative + explicit size (via .is3d) BEFORE 3Dmol createViewer, which fixes the bug where viewers rendered in the page's top-left corner. Only the cards you toggle spin up a viewer, so the WebGL-context budget isn't blown. Rotating a 3D card no longer triggers the analyze click; clicking the name/card still opens the read. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent efbf324 commit 9e99eb8

1 file changed

Lines changed: 44 additions & 33 deletions

File tree

training/workbench.html

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,15 @@
8080
.cat.on{background:var(--aroma);color:#fff;border-color:var(--aroma)}
8181
.cat.taste.on{background:var(--sweet);border-color:var(--sweet)}
8282
.browse-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
83-
.bcell{border:1px solid var(--line);border-radius:8px;padding:8px;cursor:pointer;background:#fff;text-align:center;transition:border-color .15s}
83+
.bcell{position:relative;border:1px solid var(--line);border-radius:8px;padding:8px;cursor:pointer;background:#fff;text-align:center;transition:border-color .15s}
84+
.mini-3d{position:absolute;top:6px;right:6px;z-index:2;font:inherit;font-size:10px;font-weight:600;padding:1px 6px;border:1px solid var(--line);border-radius:5px;background:#fff;color:var(--muted);cursor:pointer}
85+
.mini-3d:hover{border-color:var(--aroma);color:var(--aroma)}
86+
.bcell .bstruct{position:relative}
87+
.bcell .bstruct.is3d{height:130px}
88+
.nb-structwrap{position:relative;flex:0 0 auto}
89+
.nb-3d{top:2px;right:2px;padding:0 5px}
90+
.neighbor .nb-struct{position:relative}
91+
.neighbor .nb-struct.is3d{width:108px;height:88px}
8492
.bcell:hover{border-color:var(--aroma)}
8593
.bcell .bstruct{height:68px}.bcell .bstruct svg{max-width:100%;height:68px}
8694
.bcell .bname{font-size:12px;font-weight:600;margin-top:5px;line-height:1.25;text-transform:capitalize;cursor:pointer}
@@ -196,9 +204,7 @@ <h1>Flavormancer</h1>
196204
</div>
197205

198206
<div id="browse" class="browse">
199-
<div class="browse-head">Or browse the library — pick a category, then a molecule
200-
<span class="browse-toggle"><button type="button" id="bmode2d" class="st-tab on">2D</button><button type="button" id="bmode3d" class="st-tab">3D</button></span>
201-
</div>
207+
<div class="browse-head">Or browse the library — pick a category, then a molecule (each card has a 2D/3D toggle)</div>
202208
<div id="browseCats" class="browse-cats"></div>
203209
<div id="browseGrid" class="browse-grid"></div>
204210
</div>
@@ -388,9 +394,13 @@ <h2>Aroma</h2>
388394
if(!neighbors.length){
389395
$('neighbors').innerHTML = '<div class="empty">Build taste_master.parquet to enable substitution search.</div>';
390396
}else{
391-
$('neighbors').innerHTML = neighbors.map(x=>`
397+
window._nbrs = neighbors; // for the per-card 3D toggle handler
398+
$('neighbors').innerHTML = neighbors.map((x,i)=>`
392399
<div class="neighbor" data-smi="${x.smiles}" title="Click to analyze this molecule">
393-
<div class="nb-struct">${x.svg||''}</div>
400+
<div class="nb-structwrap">
401+
<div class="nb-struct" id="nbs${i}">${x.svg||''}</div>
402+
${_has3D?`<button type="button" class="mini-3d nb-3d" data-i="${i}" data-m="2d">3D</button>`:''}
403+
</div>
394404
<div class="nb-info">
395405
<div class="nb-name">${x.name||'—'}</div>
396406
${x.iupac?`<div class="nb-iupac">${x.iupac}</div>`:''}
@@ -520,8 +530,12 @@ <h2>Aroma</h2>
520530

521531
go.addEventListener('click', run);
522532
q.addEventListener('keydown', e=>{ if(e.key==='Enter') run(); });
523-
// click a substitution candidate -> analyze that molecule
533+
// substitution card: 3D toggle button, rotate-in-place (no analyze), or click card -> analyze
524534
$('neighbors').addEventListener('click', e=>{
535+
const btn = e.target.closest('.mini-3d');
536+
if(btn){ e.stopPropagation(); const i=+btn.dataset.i; const x=(window._nbrs||[])[i]||{};
537+
cardToggle3D(btn, $('nbs'+i), x.smiles, x.svg||''); return; }
538+
if(e.target.closest('.nb-struct.is3d')) return; // rotating the 3D model, not analyzing
525539
const nb = e.target.closest('.neighbor');
526540
if(nb && nb.dataset.smi){ q.value = nb.dataset.smi; window.scrollTo({top:0,behavior:'smooth'}); run(); }
527541
});
@@ -571,45 +585,42 @@ <h2>Aroma</h2>
571585
if(el.querySelector('.cat')) showCategory(el.querySelector('.cat')); // open first by default
572586
}catch(e){ setTimeout(initBrowse, 3000); }
573587
}
574-
let _browseMode='2d', _browseCat=null;
575588
const _has3D = typeof $3Dmol !== 'undefined';
576-
function setBrowseMode(mode){
577-
_browseMode = mode;
578-
$('bmode2d').classList.toggle('on', mode==='2d');
579-
$('bmode3d').classList.toggle('on', mode==='3d');
580-
if(_browseCat) showCategory(_browseCat);
589+
// mount an interactive 3D conformer INTO a container. The container is sized + position:relative
590+
// via CSS (.is3d) BEFORE createViewer — that's what keeps 3Dmol from rendering in the page corner.
591+
async function mount3D(el, smiles){
592+
if(!_has3D){ el.innerHTML='<div class="s3-empty">3D unavailable</div>'; return; }
593+
el.innerHTML='<div class="s3-empty">…</div>';
594+
const s3 = await post('/api/structure3d',{smiles});
595+
if(!s3 || !s3.molblock){ el.innerHTML='<div class="s3-empty">no 3D</div>'; return; }
596+
el.innerHTML='';
597+
const v = $3Dmol.createViewer(el, {backgroundColor:'white'});
598+
v.addModel(s3.molblock,'sdf'); v.setStyle({},{stick:{radius:0.13},sphere:{scale:0.23}});
599+
v.zoomTo(); v.render(); v.resize();
600+
}
601+
// per-card 2D/3D toggle: swaps a struct element between its 2D svg and an in-card 3D viewer
602+
function cardToggle3D(btn, structEl, smiles, svgHtml){
603+
if(btn.dataset.m==='3d'){ btn.dataset.m='2d'; btn.textContent='3D'; structEl.classList.remove('is3d'); structEl.innerHTML=svgHtml; }
604+
else { btn.dataset.m='3d'; btn.textContent='2D'; structEl.classList.add('is3d'); mount3D(structEl, smiles); }
581605
}
582-
$('bmode2d').addEventListener('click', ()=>setBrowseMode('2d'));
583-
$('bmode3d').addEventListener('click', ()=>setBrowseMode(_has3D?'3d':'2d'));
584606

585607
async function showCategory(btn){
586-
_browseCat = btn;
587608
document.querySelectorAll('.cat').forEach(c=>c.classList.remove('on'));
588609
btn.classList.add('on');
589-
const grid = $('browseGrid'), threeD = _browseMode==='3d' && _has3D;
610+
const grid = $('browseGrid');
590611
grid.innerHTML = '<div class="empty">Loading…</div>';
591-
const d = await getJSON('/api/top?category='+encodeURIComponent(btn.dataset.key)+'&limit='+(threeD?9:24));
612+
const d = await getJSON('/api/top?category='+encodeURIComponent(btn.dataset.key)+'&limit=24');
592613
const items = d.items||[];
593-
const esc = s => s.replace(/"/g,'&quot;');
594614
grid.innerHTML = items.map((it,i)=>`
595-
<div class="bcell" data-smi="${esc(it.smiles)}">
596-
<div class="bstruct" id="b3d${i}">${threeD?'':(it.svg||'')}</div>
615+
<div class="bcell">
616+
<div class="bstruct" id="bs${i}">${it.svg||''}</div>
617+
${_has3D?`<button type="button" class="mini-3d" data-i="${i}" data-m="2d">3D</button>`:''}
597618
<div class="bname" title="Analyze ${it.name}">${it.name}</div></div>`).join('')
598619
|| '<div class="empty">No molecules.</div>';
599-
// click the NAME to analyze (leaves the 3D structure free to rotate)
600620
grid.querySelectorAll('.bname').forEach((el,i)=> el.addEventListener('click', ()=>{
601621
q.value = items[i].smiles; window.scrollTo({top:0,behavior:'smooth'}); run(); }));
602-
if(threeD){ // render up to 9 interactive conformers (WebGL context budget)
603-
items.forEach(async (it,i)=>{
604-
const s3 = await post('/api/structure3d',{smiles:it.smiles});
605-
const el = document.getElementById('b3d'+i);
606-
if(s3 && s3.molblock && el){
607-
const v = $3Dmol.createViewer(el,{backgroundColor:'white'});
608-
v.addModel(s3.molblock,'sdf'); v.setStyle({},{stick:{radius:0.12},sphere:{scale:0.22}});
609-
v.zoomTo(); v.render(); v.resize();
610-
} else if(el){ el.innerHTML='<div class="s3-empty">no 3D</div>'; }
611-
});
612-
}
622+
grid.querySelectorAll('.mini-3d').forEach(b=>{ const i=+b.dataset.i;
623+
b.addEventListener('click', ()=> cardToggle3D(b, $('bs'+i), items[i].smiles, items[i].svg||'')); });
613624
}
614625
initBrowse();
615626

0 commit comments

Comments
 (0)