// ===================================================== // ZeMol - Visualizador de moléculas 3D // ===================================================== // --- reset global (permite rerodar sem refresh) --- try { var G = window.__PDB3D__; if (G && G.ui && G.ui.remove) G.ui.remove(); if (G && G.meta && G.meta.remove) G.meta.remove(); } catch(e){} window.__PDB3D__ = {}; // ======= Estado global ======= var __baseData = [], __hiData = [], __baseLayout = null, __lastParsed = null; var __ssData = {helix:[], sheet:[]}, __ssSel = {helix:false, sheet:false}; // OFF por padrão var __polOn = false, __polTrace = null; // sliders var __caSize = 6; // tamanho das esferas (CA/átomos) // sliders var __opacity = 1.0; // opacidade global // MOL/SDF/XYZ var __isGenericMol = false; var __molAtoms = null, __molBonds = null; (function(){ if (window.__PDB3D__.init) return; window.__PDB3D__.init = true; var host = document.getElementById("grafico"); // ---------- UI ---------- var ui = document.createElement("div"); ui.id = "pdb3d-ui2"; ui.style.margin = "6px 0"; ui.style.display = "flex"; ui.style.flexWrap = "wrap"; ui.style.gap = "8px"; ui.style.alignItems = "center"; host.parentNode.insertBefore(ui, host); window.__PDB3D__.ui = ui; function mkSpan(t){ var s=document.createElement("span"); s.textContent=t; return s; } ui.appendChild(mkSpan("Arquivo PDB/MOL/SDF/XYZ:")); var inpFile = document.createElement("input"); inpFile.type = "file"; inpFile.accept = ".pdb,.ent,.mol,.sdf,.xyz,.txt,text/plain"; ui.appendChild(inpFile); ui.appendChild(mkSpan(" ou PDB ID:")); var inpId = document.createElement("input"); inpId.type = "text"; inpId.placeholder = "ex.: 1CRN"; inpId.size = 8; ui.appendChild(inpId); var btnFetch = document.createElement("button"); btnFetch.textContent = "baixar"; ui.appendChild(btnFetch); ui.appendChild(mkSpan(" ou PubChem:")); var inpPub = document.createElement("input"); inpPub.type = "text"; inpPub.placeholder = "CID ou nome (ex.: 2244 ou caffeine)"; inpPub.size = 18; ui.appendChild(inpPub); var btnPub = document.createElement("button"); btnPub.textContent = "PubChem 3D"; ui.appendChild(btnPub); var info = document.createElement("span"); info.style.fontSize = "12px"; info.style.color = "#555"; info.textContent = " selecione arquivo ou informe ID"; ui.appendChild(info); // seleção var selBox = document.createElement("input"); selBox.type = "text"; selBox.placeholder = "ex.: Arg15, A:15-30, Asp117 | em MOL: 1-20, C,N,O"; selBox.size = 36; ui.appendChild(selBox); var btnHi = document.createElement("button"); btnHi.textContent = "destacar"; ui.appendChild(btnHi); var btnClear = document.createElement("button"); btnClear.textContent = "limpar"; ui.appendChild(btnClear); // toggles HELIX/SHEET (desmarcados por padrão) var cbHelix = document.createElement("input"); cbHelix.type = "checkbox"; cbHelix.checked = false; ui.appendChild(cbHelix); var lbHelix = document.createElement("label"); lbHelix.textContent = "hélice"; lbHelix.style.marginRight = "8px"; ui.appendChild(lbHelix); var cbSheet = document.createElement("input"); cbSheet.type = "checkbox"; cbSheet.checked = false; ui.appendChild(cbSheet); var lbSheet = document.createElement("label"); lbSheet.textContent = "folha β"; ui.appendChild(lbSheet); cbHelix.onchange = function(){ __ssSel.helix = !!cbHelix.checked; updatePlot(); }; cbSheet.onchange = function(){ __ssSel.sheet = !!cbSheet.checked; updatePlot(); }; // polaridade (PDB apenas) var btnPol = document.createElement("button"); btnPol.textContent = "polaridade"; ui.appendChild(btnPol); btnPol.onclick = function(){ if(!__isGenericMol){ __polOn = !__polOn; btnPol.style.fontWeight = __polOn ? "700":"400"; updatePlot(); } }; // slider CA size ui.appendChild(mkSpan(" CA size:")); var sliderCA = document.createElement("input"); sliderCA.type = "range"; sliderCA.min = 1; sliderCA.max = 100; sliderCA.value = __caSize; sliderCA.style.width = "120px"; ui.appendChild(sliderCA); sliderCA.oninput = function(){ __caSize = parseInt(sliderCA.value,10); updatePlot(); }; // slider opacidade ui.appendChild(mkSpan(" opacidade:")); var sliderOp = document.createElement("input"); sliderOp.type = "range"; sliderOp.min = 1; sliderOp.max = 100; sliderOp.value = Math.round(__opacity*100); sliderOp.style.width = "120px"; ui.appendChild(sliderOp); sliderOp.oninput = function(){ __opacity = Math.max(0, Math.min(1, parseInt(sliderOp.value,10)/100)); updatePlot(); }; // ---- barra de metadados ---- var metaBar = document.createElement("div"); metaBar.id = "pdb3d-meta"; metaBar.style.margin = "6px 0 10px 0"; metaBar.style.padding = "8px"; metaBar.style.border = "1px solid #ddd"; metaBar.style.borderRadius = "6px"; metaBar.style.background = "#fafafa"; metaBar.style.fontSize = "12px"; metaBar.style.lineHeight = "1.35em"; host.parentNode.insertBefore(metaBar, host); window.__PDB3D__.meta = metaBar; // wrapper var root = host.parentNode, wrap = document.getElementById("pdb3d-wrap"); if (!wrap) { wrap = document.createElement("div"); wrap.id = "pdb3d-wrap"; wrap.style.display = "grid"; wrap.style.gridTemplateRows = "auto auto 1fr"; wrap.style.gap = "8px"; wrap.style.width = "100%"; wrap.style.boxSizing = "border-box"; root.insertBefore(wrap, host); } if (ui.parentNode !== wrap) wrap.appendChild(ui); if (metaBar.parentNode !== wrap) wrap.appendChild(metaBar); if (host.parentNode !== wrap) wrap.appendChild(host); ui.style.width = "100%"; metaBar.style.width = "100%"; host.style.width = "100%"; host.style.display = "block"; host.style.clear = "both"; host.style.minHeight = "820px"; function updateMetaBar(meta, counts, extra){ var t = meta && meta.title ? meta.title : ""; var h = meta && meta.header? meta.header: ""; var txt = (__isGenericMol ? ("MOL/SDF/XYZ — átomos: "+(counts.atoms||0)+" ligações: "+(counts.bonds||0)) : (h+"--"+t+" cadeias: "+(counts.chains||0)+" ligantes: "+(counts.lig||0)+" ions: "+(counts.ions||0)+" S-S: "+(counts.ss||0)) ); metaBar.textContent = txt + (extra ? (" "+extra) : ""); } // ===== Cores e utils ===== var chainPalette = ["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"]; var ionColors = {"NA":"#1f77b4","K":"#9467bd","CA":"#2ca02c","MG":"#8c564b","ZN":"#7f7f7f","FE":"#d62728","MN":"#bcbd22","CU":"#ff7f0e","NI":"#17becf","CO":"#e377c2"}; var ionSet={}; for (var k in ionColors) ionSet[k]=true; // CPK (compacto) var CPK = { H:"#FFFFFF", C:"#909090", N:"#3050F8", O:"#FF0D0D", F:"#90E050", CL:"#1FF01F", BR:"#A62929", I:"#940094", P:"#FF8000", S:"#FFFF30", NA:"#AB5CF2", K:"#8F40D4", MG:"#8AFF00", CA:"#3DFF00", FE:"#E06633", ZN:"#7D80B0", CU:"#C88033", NI:"#50D050", SI:"#F0C8A0", SE:"#FFA100", AU:"#FFD123", AG:"#C0C0C0" }; function cpk(el){ if(!el) return "#CCCCCC"; el=String(el).toUpperCase().trim(); return CPK[el] || CPK[el.replace(/[^A-Z]/g,"")] || "#CCCCCC"; } var NL=String.fromCharCode(10), CR=String.fromCharCode(13); function splitLines(t){ var s=String(t||""); s=s.split(CR+NL).join(NL); s=s.split(CR).join(NL); return s.split(NL); } function dist2(a,b){ var dx=a.x-b.x,dy=a.y-b.y,dz=a.z-b.z; return dx*dx+dy*dy+dz*dz; } function splitSpaces(s){ var a=[],cur=""; for (var i=0;i 3 letras; já-3-letras permanece if (!tok) return null; tok = tok.toUpperCase().trim(); if (tok.length===1 && AA1[tok]) return AA1[tok]; if (AA3[tok]) return tok; return null; } function aaListFromToken(s){ // aceita “ARG”, “R”, “ARG/LYS”, “arg+lys”, “aromatic”, etc. if (!s) return null; s = s.trim(); var key = s.toLowerCase(); // grupo? if (AA_GROUPS[key]) return AA_GROUPS[key].slice(); // lista explícita (separada por , ; / +) var parts = s.split(/[,+/;s]+/).filter(Boolean); if (parts.length){ var out = []; for (var i=0;in2){var t=n1;n1=n2;n2=t;} for(var n=n1;n<=n2;n++) out.push({chain:entry.chain,n:n}); } return out; } if(entry.n!=null) out.push({chain:entry.chain,n:entry.n,aa:entry.aa}); return out; } function parseSelectionQuery(q, CA){ if (__isGenericMol) return parseSelectionForMol(q); // modo MOL/XYZ var idx = buildCAIndex(CA); var items = String(q||"").split(","); var sel = []; for (var i=0;i20? line.substr(19,1):" ").trim()||" "; var s1 =parseInt((line.length>25? line.substr(21,4):"").trim(),10); var ch2=(line.length>33? line.substr(31,1):" ").trim()||" "; var s2 =parseInt((line.length>38? line.substr(33,4):"").trim(),10); if(isFinite(s1)&&isFinite(s2)) helices.push({chain:ch1,start:s1,end:s2}); continue; } if(rec==="SHEET "){ var chS=(line.length>22? line.substr(21,1):" ").trim()||" "; var sS =parseInt((line.length>27? line.substr(22,4):"").trim(),10); var chE=(line.length>34? line.substr(32,1):" ").trim()||" "; var eS =parseInt((line.length>39? line.substr(33,4):"").trim(),10); if(isFinite(sS)&&isFinite(eS)) sheets.push({chain:chS,start:sS,end:eS}); } if(rec==="MODEL "){ if(firstModelOnly){ if(modelStarted) break; modelStarted=true; } } if(rec==="ENDMDL"){ if(firstModelOnly) break; } if(rec==="SSBOND"){ var c1=line.substr(15,1),s1b=parseInt(line.substr(17,4),10); var c2=line.substr(29,1),s2b=parseInt(line.substr(31,4),10); if(isFinite(s1b)&&isFinite(s2b)) ssBonds.push({c1:c1,r1:s1b,c2:c2,r2:s2b}); continue; } if(rec!=="ATOM "&&rec!=="HETATM") continue; if(line.length<54) continue; var name=line.substr(12,4).trim(); var resName=line.substr(17,3).trim(); var chain=line.substr(21,1)||" "; var resSeq=parseInt(line.substr(22,4),10); var iCode=line.substr(26,1).trim(); var x=parseFloat(line.substr(30,8)), y=parseFloat(line.substr(38,8)), z=parseFloat(line.substr(46,8)); var alt=line.substr(16,1); if(!isFinite(x)||!isFinite(y)||!isFinite(z)) continue; if(alt&&alt!==" "&&alt!=="A") continue; if(rec==="ATOM "){ // Proteína: CA; Nucleotídeo: P/C4' como pseudo-CA var takeAsBackbone = false; if (name==="CA") takeAsBackbone = true; else if (isNA(resName) && isNABackboneAtom(name)) takeAsBackbone = true; if (takeAsBackbone){ if(!CA[chain]) CA[chain]=[]; var obj={x:x,y:y,z:z,resName:resName,resSeq:resSeq,iCode:iCode,chain:chain}; // tenta ler valor ao fim; senão: aa→KD, NA→0 var pol=null; if (line.length>66){ var tail=line.substr(66).trim(); if (tail){ var toks=splitSpaces(tail); var last=toks.length? toks[toks.length-1] : ""; var v=parseFloat(last); if (!isNaN(v)) pol=v; } } if (pol===null) pol = isNA(resName) ? 0 : aaPolarity(resName); obj.pol=pol; CA[chain].push(obj); if(resName==="CYS" && name==="CA"){ CYS_CA[chain+":"+resSeq]={x:x,y:y,z:z}; } } if(resName==="CYS" && name==="SG"){ CYS_SG.push({chain:chain,resSeq:resSeq,x:x,y:y,z:z}); } } else { var isWater=(resName==="HOH"||resName==="WAT"||resName==="DOD"||resName==="SOL"); if (isWater) continue; var el=""; if (line.length>=78) el=line.substr(76,2).trim().toUpperCase(); if (!el) el=line.substr(12,2).trim().toUpperCase(); if (ionSet[el]){ ions.push({x:x,y:y,z:z,el:el,name:name,resName:resName,chain:chain,resSeq:resSeq}); } else { var key=chain+":"+resSeq+":"+resName; if (!ligResidues[key]) ligResidues[key]={ chain:chain, resSeq:resSeq, resName:resName, atoms:[] }; ligResidues[key].atoms.push({x:x,y:y,z:z, el:el, atom:name}); if (!ligResidues.__names) ligResidues.__names={}; ligResidues.__names[resName]=true; } } } var sPairs=[]; for(var p=0;p=start && r.resSeq<=end){ xs.push(r.x); ys.push(r.y); zs.push(r.z); txt.push("Chain "+(r.chain||" ")+" "+r.resName+" "+r.resSeq); } } return {x:xs,y:ys,z:zs,txt:txt}; } function tracesSecStruct(CA,segs,color,label){ var out=[], first=true; for (var k=0;kcmax) cmax=pol; txt.push("Chain "+r.chain+" "+r.resName+" "+r.resSeq+" | pol "+(pol.toFixed?pol.toFixed(2):pol)); } } if (!xs.length) return null; return { type:"scatter3d", mode:"markers", x:xs,y:ys,z:zs,text:txt,hoverinfo:"text", name:"polaridade (CA)", marker:{ size:5, color:cv, opacity:__opacity, colorscale:[[0,'#2166ac'],[0.5,'#f7f7f7'],[1,'#b2182b']], cmin:cmin, cmax:cmax, colorbar:{title:"polaridade"} }, showlegend:true }; } function layout3D(title){ return { title: title, scene:{ xaxis:{visible:false}, yaxis:{visible:false}, zaxis:{visible:false}, aspectmode:"data" }, margin:{l:0,r:0,t:40,b:0}, showlegend:true, legend: { // permite clicar uma única entrada "Ligantes" para esconder/mostrar todos groupclick: "togglegroup", itemclick: "toggle" } }; } // ======== MOL/SDF/XYZ parsers ======== function parseMOLorSDF(text){ var lines=splitLines(text), atoms=[], bonds=[]; var i=0, v3000=false; for(; i=lines.length) i=3; // fallback if(v3000){ var inAtoms=false,inBonds=false; for(var k=0;k=6){ atoms.push({el:t[1], x:parseFloat(t[2]), y:parseFloat(t[3]), z:parseFloat(t[4])}); } } if(inBonds && /Ms+V30s+/i.test(L)){ var b=splitSpaces(L.replace(/^.*Ms+V30s+/i,"")); if(b.length>=5){ bonds.push({a1:parseInt(b[2],10)-1, a2:parseInt(b[3],10)-1, order:parseInt(b[1],10)||1}); } } } } else { var counts = lines[i]||""; var na = parseInt((counts.substr(0,3)||"0"),10); var nb = parseInt((counts.substr(3,3)||"0"),10); var aStart=i+1, bStart=aStart+na; for(var ia=0; ia=4){ atoms.push({el:t[0],x:parseFloat(t[1]),y:parseFloat(t[2]),z:parseFloat(t[3])}); } } return {atoms:atoms,bonds:inferBonds(atoms)}; } // infer bonds por distância var RC={H:0.31,C:0.76,N:0.71,O:0.66,F:0.57,P:1.07,S:1.05,CL:1.02,BR:1.20,I:1.39}; function rc(el){el=(el||"").toUpperCase(); return (RC[el]!=null?RC[el]:0.77);} function inferBonds(at){ var b=[], n=at.length; for(var i=0;i=0 && i<__molAtoms.length && !idxSel[i]){ idxSel[i]=1; ret.push(__molAtoms[i]); } } for(var i=0;ib){var tt=a;a=b;b=tt;} for(var k=a;k<=b;k++) add(k); continue; } if(/^[0-9]+$/.test(p)){ add(parseInt(p,10)-1); continue; } var list=p.replace(/s+/g,"").split(/[,;]+/).filter(Boolean); if(list.length){ var set={}; list.forEach(function(e){ set[e.toUpperCase()]=1; }); for(var j=0;j<__molAtoms.length;j++){ if(set[(__molAtoms[j].el||"").toUpperCase()]) add(j); } } } return ret; } // ---------- Render ---------- function renderFromPDB(txt,title){ __isGenericMol=false; var meta=extractHeaderTitle(txt), parsed=parsePDB(txt), ligList=parsed.ligNames||[]; var base=[] .concat(tracesCA(parsed.CA)) .concat(tracesSS(parsed.sPairs)) .concat(tracesLig(parsed.ligResidues)) .concat(tracesIons(parsed.ions)); if(!base.length){ info.textContent=" nenhum dado plotavel"; Plotly.react("grafico",[ {type:"scatter",x:[0],y:[0],mode:"text",text:["PDB sem dados"]} ],{title:"Sem dados"}); updateMetaBar(meta,{chains:0,lig:0,ions:0,ss:0}); return; } __baseData=base; __baseLayout=layout3D(title); __hiData=[]; __lastParsed=parsed; __ssData.helix=tracesSecStruct(parsed.CA,parsed.helices,"#ff0000","hélice α"); __ssData.sheet=tracesSecStruct(parsed.CA,parsed.sheets, "#ffa500","folha β"); __polTrace = makePolarityTrace(parsed.CA); var nChains=Object.keys(parsed.CA).length; var nLig=Object.keys(parsed.ligResidues).length; if(parsed.ligResidues.__names) nLig--; updateMetaBar(meta,{chains:nChains,lig:nLig,ions:parsed.ions.length,ss:parsed.sPairs.length}, (ligList.length?("lig: ["+ligList.join(", ")+"]"):"")); fadeBaseData(false); __baseData.forEach(function(tr){ tr.__isBase = true; }); updatePlot(); info.textContent=" pronto: "+title.replace(/^PDB:s*/,""); } function renderFromMol(text, title){ __isGenericMol=true; __polOn=false; __polTrace=null; __hiData=[]; var mol=parseMOLorSDF(text); if(!mol.atoms.length){ info.textContent=" arquivo sem átomos reconhecíveis"; Plotly.react("grafico",[ {type:"scatter",x:[0],y:[0],mode:"text",text:["Sem dados"]} ],{title:"Sem dados"}); return; } __molAtoms=mol.atoms; __molBonds=mol.bonds||inferBonds(mol.atoms); __baseData=tracesMol(__molAtoms,__molBonds); __baseLayout=layout3D(title); __lastParsed=null; __ssData.helix=[]; __ssData.sheet=[]; updateMetaBar(null,{atoms:__molAtoms.length,bonds:__molBonds.length}); Plotly.react("grafico", __baseData, __baseLayout); info.textContent=" pronto: "+title; } function renderFromXYZ(text, title){ __isGenericMol=true; __polOn=false; __polTrace=null; __hiData=[]; var mol=parseXYZ(text); __molAtoms=mol.atoms; __molBonds=mol.bonds; __baseData=tracesMol(__molAtoms,__molBonds); __baseLayout=layout3D(title); __lastParsed=null; __ssData.helix=[]; __ssData.sheet=[]; updateMetaBar(null,{atoms:__molAtoms.length,bonds:__molBonds.length}); Plotly.react("grafico", __baseData, __baseLayout); info.textContent=" pronto: "+title; } // depois de montar __baseData: __baseData.forEach(function(tr){ tr.__isBase = true; }); function __applySlidersToTraces(allTraces, fadeOn){ // fadeOn = true quando polaridade/destaque estiverem ativos (deixa base mais clarinha) var baseOpacity = __opacity; var fadedOpacity = Math.min(0.25, baseOpacity); for (var i=0;i3D flat)"); }) .catch(function(){ info.textContent = " erro ao baixar do PubChem: "+q; Plotly.react("grafico", [{type:"scatter",x:[0],y:[0],mode:"text",text:["Nao foi possivel baixar do PubChem"]}], {title:"Erro PubChem"} ); }); }); }; btnFetch.onclick=function(){ var raw=(inpId.value||"").trim(); if(!raw){ info.textContent=" informe um ID"; return; } var idU=raw.toUpperCase(), idL=raw.toLowerCase(); info.textContent=" baixando "+idU+" ..."; var url1="https://files.rcsb.org/download/"+idU+".pdb"; var url1b="https://files.rcsb.org/view/"+idU+".pdb"; var url2="https://www.ebi.ac.uk/pdbe/entry-files/download/pdb"+idL+".ent"; fetch(url1).then(function(r){ if(!r.ok) throw 0; return r.text(); }) .then(function(txt){ return renderFromPDB(txt,"PDB: "+idU+".pdb"); }) .catch(function(){ return fetch(url1b).then(function(r1b){ if(!r1b.ok) throw 0; return r1b.text(); }) .then(function(txt1b){ return renderFromPDB(txt1b,"PDB: "+idU+".pdb"); }) .catch(function(){ return fetch(url2).then(function(r2){ if(!r2.ok) throw 0; return r2.text(); }) .then(function(txt2){ return renderFromPDB(txt2,"PDB: "+idU); }) .catch(function(){ info.textContent = " erro ao baixar "+idU; Plotly.react("grafico", [{type:"scatter",x:[0],y:[0],mode:"text",text:["Nao foi possivel baixar "+idU]}], {title:"Erro download"}); }); }); }); }; // retorno inicial var data0=[{type:"scatter",mode:"text",x:[0],y:[0],text:["Use upload (PDB/MOL/SDF/XYZ) ou informe um ID e clique em baixar."],hoverinfo:"none",showlegend:false}]; var layout0={title:"PDB/MOL 3D viewer",margin:{l:0,r:0,t:40,b:0}}; Plotly.react("grafico", data0, layout0); })(); // fim IIFE