STEM 3D Chemistry Viewer

STEM 3D Chemistry Viewer script details
Type
Typescript logo image
typescript
App Version
0.19.7
Visibility
public
Date Created
Mar 9, 2025, 11:24:52 PM
Last Edit Date
Mar 19, 2025, 11:50:26 AM

Script Details

The Code
// ### Element Properties // Defines colors and sizes for different elements const elementProps: { [key: string]: { color: BABYLON.Color3; radius: number; covalentRadius: number } } = { 'H': { color: BABYLON.Color3.White(), radius: 0.3 * 0.8, covalentRadius: 0.31 }, 'C': { color: BABYLON.Color3.Black(), radius: 0.7 * 0.8, covalentRadius: 0.76 }, 'O': { color: BABYLON.Color3.Red(), radius: 0.6 * 0.8, covalentRadius: 0.66 }, 'N': { color: BABYLON.Color3.Blue(), radius: 0.65 * 0.8, covalentRadius: 0.71 }, 'S': { color: BABYLON.Color3.Yellow(), radius: 0.75 * 0.8, covalentRadius: 1.05 }, 'P': { color: new BABYLON.Color3(1, 0.5, 0), radius: 0.8 * 0.8, covalentRadius: 1.07 }, 'Cl': { color: BABYLON.Color3.Green(), radius: 0.7 * 0.8, covalentRadius: 1.02 }, 'F': { color: new BABYLON.Color3(0.5, 1, 0.5), radius: 0.6 * 0.8, covalentRadius: 0.57 }, 'Br': { color: new BABYLON.Color3(0.65, 0.16, 0.16), radius: 0.85 * 0.8, covalentRadius: 1.20 }, 'I': { color: new BABYLON.Color3(0.58, 0.0, 0.83), radius: 0.95 * 0.8, covalentRadius: 1.39 }, 'Na': { color: new BABYLON.Color3(0.67, 0.36, 0.94), radius: 1.54 * 0.8, covalentRadius: 1.66 }, 'K': { color: new BABYLON.Color3(0.56, 0.25, 0.83), radius: 1.96 * 0.8, covalentRadius: 2.03 }, 'Ca': { color: new BABYLON.Color3(0.24, 1.0, 0.0), radius: 1.74 * 0.8, covalentRadius: 1.76 }, 'Fe': { color: new BABYLON.Color3(0.87, 0.39, 0.19), radius: 1.26 * 0.8, covalentRadius: 1.32 }, 'Mg': { color: new BABYLON.Color3(0.54, 1.0, 0.0), radius: 1.45 * 0.8, covalentRadius: 1.41 }, 'Zn': { color: new BABYLON.Color3(0.49, 0.5, 0.69), radius: 1.31 * 0.8, covalentRadius: 1.22 }, 'Si': { color: new BABYLON.Color3(0.94, 0.78, 0.63), radius: 1.17 * 0.8, covalentRadius: 1.11 }, 'B': { color: new BABYLON.Color3(1.0, 0.71, 0.71), radius: 0.85 * 0.8, covalentRadius: 0.84 }, }; const defaultProps = { color: BABYLON.Color3.Gray(), radius: 0.5 * 0.8, covalentRadius: 0.7 }; // ### Helper Functions // Array of element symbols, indexed by atomic number (index 0 is unused) const symbols: string[] = [ '', // 0 is unused 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og' ]; // Extended function to convert atomic number to element symbol function atomicNumberToSymbol(num: number): string { // Check for valid atomic numbers if (!Number.isInteger(num) || num < 1 || num > 118) { return 'Unknown'; } return symbols[num]; } // Parses a chemical formula into an array of atoms function parseFormula(formula: string): string[] { const regex = /([A-Z][a-z]?)(\d*)/g; const atoms: string[] = []; let match; while ((match = regex.exec(formula)) !== null) { const element = match[1]; const count = match[2] ? parseInt(match[2]) : 1; for (let i = 0; i < count; i++) { atoms.push(element); } } return atoms; } // Polls PubChem API for results when data is not immediately available async function pollForResult(listKey: string): Promise<any> { const delay = 2000; const maxAttempts = 10; let attempts = 0; while (attempts < maxAttempts) { const response = await fetch(`https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/listkey/${listKey}/cids/JSON`); const data = await response.json(); if (data.Waiting) { await new Promise(resolve => setTimeout(resolve, delay)); attempts++; } else if (data.IdentifierList && data.IdentifierList.CID) { return data.IdentifierList.CID; } else { throw new Error('Failed to retrieve results'); } } throw new Error('Request timed out'); } // Fetches 3D molecule data from PubChem API async function fetchMoleculeData(formula: string): Promise<{ atoms: string[], positions: BABYLON.Vector3[], bonds: [number, number][], bondOrders: number[] } | null> { try { const cidResponse = await fetch(`https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/formula/${formula}/cids/JSON`); const cidData = await cidResponse.json(); let cid: number; if (cidData.Waiting) { const listKey = cidData.Waiting.ListKey; const cids = await pollForResult(listKey); if (!cids || cids.length === 0) return null; cid = cids[0]; } else if (cidData.IdentifierList && cidData.IdentifierList.CID) { cid = cidData.IdentifierList.CID[0]; } else { return null; } const recordResponse = await fetch(`https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${cid}/record/JSON?record_type=3d`); const recordData = await recordResponse.json(); if (!recordData.PC_Compounds || recordData.PC_Compounds.length === 0) return null; const compound = recordData.PC_Compounds[0]; const atoms = compound.atoms.element.map((num: number) => atomicNumberToSymbol(num)); const coords = compound.coords.find((c: any) => c.type.includes(5)); if (!coords || !coords.conformers) return null; const conformer = coords.conformers[0]; const positions = conformer.x.map((x: number, i: number) => new BABYLON.Vector3(x, conformer.y[i], conformer.z[i])); const bonds = compound.bonds.aid1.map((aid1: number, i: number) => [aid1 - 1, compound.bonds.aid2[i] - 1] as [number, number]); const bondOrders = compound.bonds.order ? compound.bonds.order.map((order: string) => parseFloat(order)) : bonds.map(() => 1); return { atoms, positions, bonds, bondOrders }; } catch (error) { console.error('Error fetching data:', error); return null; } } // Creates a simplified linear molecule when API data is unavailable function createSimplifiedMolecule(atoms: string[], scene: BABYLON.Scene, atomSizeScale: number = 1.0, parent: BABYLON.TransformNode | null = null): void { let x = 0; const spacing = 2; atoms.forEach(atom => { const props = elementProps[atom] || defaultProps; const sphere = BABYLON.MeshBuilder.CreateSphere(atom, { diameter: props.radius * 2 * atomSizeScale }, scene); sphere.position.x = x; if (parent) sphere.parent = parent; const material = new BABYLON.PBRMaterial(`${atom}_mat`, scene); material.albedoColor = props.color; sphere.material = material; x += spacing; }); } // Creates a detailed molecule visualization with atoms and bonds async function createMolecule( formula: string, scene: BABYLON.Scene, atomSizeScale: number = 1.0, bondThickness: number = 0.1, bondSplit: boolean = true, parent: BABYLON.TransformNode | null = null ): Promise<void> { const data = await fetchMoleculeData(formula); errorText.text = ""; if (!data) { errorText.text = "Molecule or it's 3D definition not found in PubChem. Falling back to simplified model."; const atoms = parseFormula(formula); createSimplifiedMolecule(atoms, scene, atomSizeScale, parent); return; } const { atoms, positions, bonds, bondOrders } = data; // Create materials for each element const materials: { [key: string]: BABYLON.PBRMetallicRoughnessMaterial } = {}; Array.from(new Set(atoms)).forEach(element => { const props = elementProps[element] || defaultProps; const material = new BABYLON.PBRMetallicRoughnessMaterial(`${element}_mat`, scene); material.baseColor = props.color; material.metallic = 0.9; material.roughness = 0.26; materials[element] = material; }); // Render atoms as spheres atoms.forEach((atom, i) => { const props = elementProps[atom] || defaultProps; const sphere = BABYLON.MeshBuilder.CreateSphere(`${atom}_${i}`, { diameter: props.radius * 2 * atomSizeScale }, scene); sphere.position = positions[i]; if (parent) sphere.parent = parent; sphere.material = materials[atom]; }); // Render bonds as cylinders bonds.forEach(([i, j], index) => { const order = bondOrders[index]; const p1 = positions[i]; const p2 = positions[j]; const atom1 = atoms[i]; const atom2 = atoms[j]; const props1 = elementProps[atom1] || defaultProps; const props2 = elementProps[atom2] || defaultProps; const distance = BABYLON.Vector3.Distance(p1, p2); const direction = p2.subtract(p1).normalize(); const midpoint = BABYLON.Vector3.Lerp(p1, p2, 0.5); const expectedLength = (props1.covalentRadius + props2.covalentRadius) * 1.1; const scaleFactor = Math.min(1, expectedLength / distance); const adjustedP1 = p1; const adjustedP2 = p1.add(direction.scale(distance * scaleFactor)); const adjustedMidpoint = BABYLON.Vector3.Lerp(adjustedP1, adjustedP2, 0.5); const thickness = bondThickness * (order === 1 ? 1 : order === 1.5 ? 1.2 : order === 2 ? 1.5 : 2); const createBondHalf = (start: BABYLON.Vector3, end: BABYLON.Vector3, color: BABYLON.Color3, offset: BABYLON.Vector3) => { const halfLength = BABYLON.Vector3.Distance(start, end); const cylinder = BABYLON.MeshBuilder.CreateCylinder(`bond_${index}`, { height: halfLength, diameter: thickness, tessellation: 16 }, scene); if (parent) cylinder.parent = parent; const material = new BABYLON.PBRMetallicRoughnessMaterial(`bond_mat_${index}`, scene); material.baseColor = color; cylinder.material = material; const mid = BABYLON.Vector3.Center(start, end); cylinder.position = mid.add(offset); const v = end.subtract(start).normalize(); if (Math.abs(v.y) > 0.999) { cylinder.rotationQuaternion = v.y > 0 ? BABYLON.Quaternion.Identity() : BABYLON.Quaternion.RotationAxis(BABYLON.Vector3.Right(), Math.PI); } else { const axis = BABYLON.Vector3.Cross(BABYLON.Vector3.Up(), v).normalize(); const angle = Math.acos(BABYLON.Vector3.Dot(BABYLON.Vector3.Up(), v)); cylinder.rotationQuaternion = BABYLON.Quaternion.RotationAxis(axis, angle); } return cylinder; }; const perpendicular = direction.cross(new BABYLON.Vector3(0, 0, 1)).normalize().scale(thickness * 1.5); if (order === 1 || order === 1.5) { if (bondSplit) { createBondHalf(adjustedP1, adjustedMidpoint, props1.color, BABYLON.Vector3.Zero()); createBondHalf(adjustedMidpoint, adjustedP2, props2.color, BABYLON.Vector3.Zero()); } else { createBondHalf(adjustedP1, adjustedP2, BABYLON.Color3.Gray(), BABYLON.Vector3.Zero()); } } else if (order === 2) { [-1, 1].forEach(sign => { const offset = perpendicular.scale(sign); if (bondSplit) { createBondHalf(adjustedP1, adjustedMidpoint, props1.color, offset); createBondHalf(adjustedMidpoint, adjustedP2, props2.color, offset); } else { createBondHalf(adjustedP1, adjustedP2, BABYLON.Color3.Red(), offset); } }); } else if (order === 3) { if (bondSplit) { createBondHalf(adjustedP1, adjustedMidpoint, props1.color, BABYLON.Vector3.Zero()); createBondHalf(adjustedMidpoint, adjustedP2, props2.color, BABYLON.Vector3.Zero()); [-1, 1].forEach(sign => { const offset = perpendicular.scale(sign); createBondHalf(adjustedP1, adjustedMidpoint, props1.color, offset); createBondHalf(adjustedMidpoint, adjustedP2, props2.color, offset); }); } else { createBondHalf(adjustedP1, adjustedP2, BABYLON.Color3.Blue(), BABYLON.Vector3.Zero()); [-1, 1].forEach(sign => { createBondHalf(adjustedP1, adjustedP2, BABYLON.Color3.Blue(), perpendicular.scale(sign)); }); } } }); } // ### Scene Setup const scene = bitbybit.babylon.scene.getScene(); const moleculeTransform = new BABYLON.TransformNode("moleculeTransform", scene); // Add lighting const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene); light.intensity = 1; const lightOpt = new Bit.Inputs.BabylonScene.DirectionalLightDto(); lightOpt.intensity = 10; bitbybit.babylon.scene.drawDirectionalLight(lightOpt); // ### GUI Setup const advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI"); const hardwareScaling = scene.getEngine().getHardwareScalingLevel(); // Get the current scaling level (0.5 in your case) // advancedTexture.renderScale = hardwareScaling const uiContainer = new BABYLON.GUI.Rectangle(); uiContainer.width = "50%"; uiContainer.height = "500px"; uiContainer.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; uiContainer.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; const stackPanel = new BABYLON.GUI.StackPanel(); stackPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; stackPanel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; // uiContainer.addControl(stackPanel); advancedTexture.addControl(stackPanel); const paddingInp = 10; const formulaInput = new BABYLON.GUI.InputText("formulaInput"); formulaInput.width = "400px"; formulaInput.height = "80px"; formulaInput.text = "C10H13N5O4"; formulaInput.color = "#ffffff"; formulaInput.fontSize = 24; formulaInput.paddingTopInPixels = paddingInp; formulaInput.paddingBottomInPixels = paddingInp; stackPanel.addControl(formulaInput); const loadButton = BABYLON.GUI.Button.CreateSimpleButton("loadButton", "Load Molecule"); loadButton.width = "400px"; loadButton.height = "80px"; loadButton.color = "#1f1c1b"; loadButton.background = "#f0cebb" loadButton.fontSize = 32; loadButton.paddingTopInPixels = paddingInp; loadButton.paddingBottomInPixels = paddingInp; stackPanel.addControl(loadButton); const sliderLabel = new BABYLON.GUI.TextBlock("sliderLabel", "Rotation Speed"); sliderLabel.height = "50px"; sliderLabel.color = "white"; stackPanel.addControl(sliderLabel); const rotationSlider = new BABYLON.GUI.Slider(); rotationSlider.minimum = 0; rotationSlider.maximum = 2 * Math.PI; rotationSlider.value = 0.5; rotationSlider.height = "20px"; rotationSlider.width = "400px"; stackPanel.addControl(rotationSlider); const errorText = new BABYLON.GUI.TextBlock("errorText", ""); errorText.color = "red"; errorText.height = "40px"; stackPanel.addControl(errorText); // New PubChem Button const pubchemButton = BABYLON.GUI.Button.CreateSimpleButton("pubchemButton", "Copy URL To PubChem"); pubchemButton.width = "400px"; pubchemButton.height = "50px"; pubchemButton.color = "white"; stackPanel.addControl(pubchemButton); // Open PubChem Page on Button Click pubchemButton.onPointerUpObservable.add(() => { const formula = formulaInput.text.trim(); if (!formula) return; const pubchemUrl = `https://pubchem.ncbi.nlm.nih.gov/#query=${encodeURIComponent(formula)}`; navigator.clipboard.writeText(pubchemUrl); }); const loadingText = new BABYLON.GUI.TextBlock("loadingText", "Loading..."); loadingText.color = "white"; loadingText.fontSize = 32; loadingText.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; loadingText.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; loadingText.isVisible = false; advancedTexture.addControl(loadingText); const imageOpt = new Bit.Inputs.BabylonGui.CreateImageDto(); imageOpt.url = "assets/logo-gold-small.png"; const image = bitbybit.babylon.gui.image.createImage(imageOpt); const padding = 30; image.paddingTopInPixels = padding; image.paddingBottomInPixels = padding; image.paddingLeftInPixels = padding; image.paddingRightInPixels = padding; image.onPointerClickObservable.add(() => { window.open("https://bitbybit.dev", '_blank').focus(); }); stackPanel.addControl(image); const txtBlockOptions1 = new Bit.Inputs.BabylonGui.CreateTextBlockDto(); txtBlockOptions1.text = "BITBYBIT.DEV"; const txtBlock1 = bitbybit.babylon.gui.textBlock.createTextBlock(txtBlockOptions1); txtBlock1.height = "40px"; txtBlock1.paddingBottomInPixels = 10; stackPanel.addControl(txtBlock1); const warningText = new BABYLON.GUI.TextBlock("warningText", "Warning: This Application is meant to be used purely for educational STEM purposes. Always double-check results with official sources. Data fetched from PubChem database."); warningText.color = "white"; warningText.fontSize = 24; warningText.width = "100%"; warningText.height = "40px"; stackPanel.addControl(warningText); // ### Rotation Logic bitbybit.time.registerRenderFunction(() => { const deltaTime = scene.getEngine().getDeltaTime() / 1000; moleculeTransform.rotation.y += rotationSlider.value * deltaTime; }); // ### Load Molecule on Button Click loadButton.onPointerUpObservable.add(async () => { const formula = formulaInput.text.trim(); if (!formula) return; loadingText.isVisible = true; errorText.text = ""; moleculeTransform.getChildMeshes().forEach(mesh => mesh.dispose()); try { await createMolecule(formula, scene, 1.0, 0.1, true, moleculeTransform); } catch (error) { errorText.text = "Molecule not found or error occurred."; } finally { loadingText.isVisible = false; } }); const start = async () => { const formula = formulaInput.text.trim(); if (!formula) return; loadingText.isVisible = true; errorText.text = ""; moleculeTransform.getChildMeshes().forEach(mesh => mesh.dispose()); try { await createMolecule(formula, scene, 1.0, 0.1, true, moleculeTransform); } catch (error) { errorText.text = "Molecule not found or error occurred."; } finally { loadingText.isVisible = false; } } start(); //C19H28O2 //C32H18N8 //C15H10O7