/** * Electron Configuration System * Manages electron orbitals and configurations for atoms */ /** * Represents a single electron orbital */ class Orbital { constructor(n, l, m) { this.n = n; // Principal quantum number this.l = l; // Azimuthal quantum number this.m = m; // Magnetic quantum number this.electrons = 0; // Number of electrons in this orbital this.visible = true; // Whether to display this orbital this.particles = []; // Array of particle positions this.particleSystem = null; // Three.js Points object this.color = null; // Custom color for this orbital (null = use default type color) } /** * Get orbital designation (e.g., "1s", "2p", "3d") * @returns {string} */ getDesignation() { const subshells = ['s', 'p', 'd', 'f', 'g', 'h', 'i']; return `${this.n}${subshells[this.l]}`; } /** * Get orbital type * @returns {string} */ getType() { const types = ['s', 'p', 'd', 'f', 'g', 'h', 'i']; return types[this.l]; } /** * Get unique identifier * @returns {string} */ getId() { return `${this.n}_${this.l}_${this.m}`; } } /** * Represents the complete electron configuration for an atom */ class ElectronConfiguration { constructor(atomicNumber) { this.atomicNumber = Math.max(1, Math.min(CONSTANTS.MAX_ATOMIC_NUMBER, atomicNumber)); this.orbitals = []; this.elementSymbol = this.getElementSymbol(this.atomicNumber); this.elementName = this.getElementName(this.atomicNumber); } /** * Build electron configuration using Aufbau principle * Fills orbitals in order of increasing energy */ build() { this.orbitals = []; console.log(`Building configuration for ${this.elementSymbol} (Z=${this.atomicNumber})`); // Aufbau principle ordering: (n+l, n) // Generate all orbitals up to a reasonable limit const orbitalOrder = []; for (let n = 1; n <= CONSTANTS.MAX_N; n++) { for (let l = 0; l < n && l <= 3; l++) { // Limit to f orbitals for (let m = -l; m <= l; m++) { orbitalOrder.push({ n, l, m, energy: n + l }); } } } console.log(`Generated ${orbitalOrder.length} total orbitals`); // Sort by energy (n+l), then by n orbitalOrder.sort((a, b) => { if (a.energy !== b.energy) return a.energy - b.energy; return a.n - b.n; }); let remainingElectrons = this.atomicNumber; // Group orbitals by subshell (n, l) for Hund's rule const subshells = new Map(); for (const { n, l, m, energy } of orbitalOrder) { const key = `${n}_${l}`; if (!subshells.has(key)) { subshells.set(key, { n, l, energy, orbitals: [] }); } subshells.get(key).orbitals.push(m); } console.log(`Grouped into ${subshells.size} subshells`); // Fill orbitals following Hund's rule const subshellArray = Array.from(subshells.values()); subshellArray.sort((a, b) => { if (a.energy !== b.energy) return a.energy - b.energy; return a.n - b.n; }); // First pass: determine which subshells have electrons and find outer shell let maxN = 0; const filledSubshells = []; let tempRemainingElectrons = this.atomicNumber; for (const subshell of subshellArray) { if (tempRemainingElectrons <= 0) break; const { n, l, orbitals: mValues } = subshell; const numOrbitals = mValues.length; const maxElectronsInSubshell = numOrbitals * 2; const electronsInSubshell = Math.min(tempRemainingElectrons, maxElectronsInSubshell); if (electronsInSubshell > 0) { filledSubshells.push({ ...subshell, electronsInSubshell }); if (n > maxN) { maxN = n; } } tempRemainingElectrons -= electronsInSubshell; } console.log(`Outer shell (valence): n=${maxN}`); // Second pass: create orbitals with proper visibility for (const subshell of filledSubshells) { const { n, l, orbitals: mValues, electronsInSubshell } = subshell; const numOrbitals = mValues.length; console.log(`Processing subshell ${n}${['s','p','d','f'][l]}: ${numOrbitals} orbitals, ${electronsInSubshell} electrons`); console.log(` m values:`, mValues); // Apply Hund's rule: fill orbitals singly first, then pair const electronsPerOrbital = new Array(numOrbitals).fill(0); // First pass: put one electron in each orbital (up to numOrbitals electrons) const singleElectrons = Math.min(electronsInSubshell, numOrbitals); for (let i = 0; i < singleElectrons; i++) { electronsPerOrbital[i] = 1; } // Second pass: pair up remaining electrons const remainingToPair = electronsInSubshell - singleElectrons; for (let i = 0; i < remainingToPair; i++) { electronsPerOrbital[i]++; } console.log(` electrons per orbital:`, electronsPerOrbital); // Create orbital objects - CREATE ALL ORBITALS IN SUBSHELL, not just filled ones for (let i = 0; i < numOrbitals; i++) { const m = mValues[i]; const orbital = new Orbital(n, l, m); orbital.electrons = electronsPerOrbital[i]; // Only show outer shell (valence shell) by default for performance // Inner shells can be enabled manually by the user orbital.visible = (n === maxN); this.orbitals.push(orbital); console.log(` Created orbital: ${orbital.getId()} with ${orbital.electrons} electrons, visible: ${orbital.visible}`); } } console.log(`Final configuration: ${this.orbitals.length} orbitals created`); console.log(`Outer shell: n=${maxN} (only outer shell visible by default for performance)`); console.log('Orbitals:', this.orbitals.map(o => `${o.getId()}(${o.electrons}e, ${o.visible ? 'visible' : 'hidden'})`).join(', ')); } /** * Get element symbol from atomic number * @param {number} atomicNumber * @returns {string} */ getElementSymbol(atomicNumber) { const elements = [ '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' ]; return elements[atomicNumber - 1] || 'Unknown'; } /** * Get element name from atomic number * @param {number} atomicNumber * @returns {string} */ getElementName(atomicNumber) { const names = [ 'Hydrogen', 'Helium', 'Lithium', 'Beryllium', 'Boron', 'Carbon', 'Nitrogen', 'Oxygen', 'Fluorine', 'Neon', 'Sodium', 'Magnesium', 'Aluminum', 'Silicon', 'Phosphorus', 'Sulfur', 'Chlorine', 'Argon', 'Potassium', 'Calcium', 'Scandium', 'Titanium', 'Vanadium', 'Chromium', 'Manganese', 'Iron', 'Cobalt', 'Nickel', 'Copper', 'Zinc', 'Gallium', 'Germanium', 'Arsenic', 'Selenium', 'Bromine', 'Krypton', 'Rubidium', 'Strontium', 'Yttrium', 'Zirconium', 'Niobium', 'Molybdenum', 'Technetium', 'Ruthenium', 'Rhodium', 'Palladium', 'Silver', 'Cadmium', 'Indium', 'Tin', 'Antimony', 'Tellurium', 'Iodine', 'Xenon', 'Cesium', 'Barium', 'Lanthanum', 'Cerium', 'Praseodymium', 'Neodymium', 'Promethium', 'Samarium', 'Europium', 'Gadolinium', 'Terbium', 'Dysprosium', 'Holmium', 'Erbium', 'Thulium', 'Ytterbium', 'Lutetium', 'Hafnium', 'Tantalum', 'Tungsten', 'Rhenium', 'Osmium', 'Iridium', 'Platinum', 'Gold', 'Mercury', 'Thallium', 'Lead', 'Bismuth', 'Polonium', 'Astatine', 'Radon', 'Francium', 'Radium', 'Actinium', 'Thorium', 'Protactinium', 'Uranium', 'Neptunium', 'Plutonium', 'Americium', 'Curium', 'Berkelium', 'Californium', 'Einsteinium', 'Fermium', 'Mendelevium', 'Nobelium', 'Lawrencium', 'Rutherfordium', 'Dubnium', 'Seaborgium', 'Bohrium', 'Hassium', 'Meitnerium', 'Darmstadtium', 'Roentgenium', 'Copernicium', 'Nihonium', 'Flerovium', 'Moscovium', 'Livermorium', 'Tennessine', 'Oganesson' ]; return names[atomicNumber - 1] || 'Unknown'; } /** * Get electron configuration string (e.g., "1s² 2s² 2p²") * @returns {string} */ getConfigurationString() { // Group orbitals by subshell (n, l) const subshells = new Map(); this.orbitals.forEach(orbital => { const key = `${orbital.n}${orbital.getType()}`; if (!subshells.has(key)) { subshells.set(key, { n: orbital.n, l: orbital.l, electrons: 0 }); } subshells.get(key).electrons += orbital.electrons; }); // Sort by energy (n+l, then n) const sortedSubshells = Array.from(subshells.entries()).sort((a, b) => { const [, shellA] = a; const [, shellB] = b; const energyA = shellA.n + shellA.l; const energyB = shellB.n + shellB.l; if (energyA !== energyB) return energyA - energyB; return shellA.n - shellB.n; }); // Build configuration string with line breaks // Group by principal quantum number (n) for better readability const configParts = sortedSubshells .filter(([, shell]) => shell.electrons > 0) .map(([key, shell]) => `${key}${this.toSuperscript(shell.electrons)}`); // Add line breaks after every 4 subshells or when n changes significantly let result = ''; let currentN = 0; let count = 0; configParts.forEach((part, index) => { const n = parseInt(part.charAt(0)); // Add line break if we've added 4 items or if n increased by 2 or more if (index > 0 && (count >= 4 || (n - currentN >= 2))) { result += '\n'; count = 0; } result += (count > 0 ? ' ' : '') + part; currentN = n; count++; }); return result; } /** * Convert number to superscript * @param {number} num * @returns {string} */ toSuperscript(num) { const superscripts = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; return num.toString().split('').map(d => superscripts[parseInt(d)]).join(''); } /** * Get total number of electrons * @returns {number} */ getTotalElectrons() { return this.orbitals.reduce((sum, orbital) => sum + orbital.electrons, 0); } /** * Get orbitals grouped by shell (n value) * @returns {Object} */ getOrbitalsByShell() { const shells = {}; this.orbitals.forEach(orbital => { if (!shells[orbital.n]) { shells[orbital.n] = []; } shells[orbital.n].push(orbital); }); return shells; } } // Make classes available globally window.Orbital = Orbital; window.ElectronConfiguration = ElectronConfiguration;