use crate::material::*;
use crate::crystal::{Crystal, LatticeType};
use crate::element::Element;
use crate::molecular::Molecule;
use crate::polymer::PolymerChain;
use crate::substance::Substance;
pub fn compile(substance: &Substance) -> MaterialDef {
match substance {
Substance::Molecular(mol) => compile_molecular(mol),
Substance::Crystalline(c) => compile_crystal(c),
Substance::Polymer(p) => compile_polymer(p),
Substance::Amorphous { name, composition, density } => {
compile_amorphous(name, composition, *density)
}
}
}
pub fn compile_as(substance: &Substance, name: &str, phase: PhaseState) -> MaterialDef {
let mut mat = compile(substance);
mat.name = name.into();
adjust_for_phase(&mut mat, substance, phase);
mat
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CrystalClass {
Metallic,
Covalent,
Ionic,
}
fn classify_crystal(c: &Crystal) -> CrystalClass {
if c.base.is_metal() && c.lattice != LatticeType::Diamond {
CrystalClass::Metallic
} else if c.lattice == LatticeType::Diamond || c.lattice == LatticeType::Ionic {
if c.base.electronegativity() > 2.0 || c.lattice == LatticeType::Diamond {
CrystalClass::Covalent
} else {
CrystalClass::Ionic
}
} else {
CrystalClass::Metallic
}
}
fn compile_molecular(mol: &Molecule) -> MaterialDef {
let bond_energy = mol.total_bond_energy();
let mp_c = estimate_molecular_melting(mol);
let bp_c = estimate_molecular_boiling(mol, mp_c);
let room_temp = 25.0;
let state = if room_temp < mp_c {
PhaseState::Solid
} else if room_temp < bp_c {
PhaseState::Liquid
} else {
PhaseState::Gas
};
let density = estimate_molecular_density(mol, state);
let specific_heat = estimate_molecular_specific_heat(mol, state);
let thermal_conductivity = match state {
PhaseState::Liquid => 0.6,
PhaseState::Gas => 0.025,
_ => 0.5,
};
let viscosity = if state == PhaseState::Liquid {
estimate_viscosity(mp_c)
} else {
0.0
};
let optical = estimate_optical(
&Substance::Molecular(mol.clone()),
state,
false,
density,
);
MaterialDef {
name: mol.name.clone(),
structural: StructuralProps {
density,
yield_strength: 0.0,
..Default::default()
},
thermal: ThermalProps {
specific_heat,
thermal_conductivity,
melting_point: Some(mp_c),
vaporisation_point: Some(bp_c),
freezing_point: Some(mp_c),
combustion_energy: if mol.atoms.contains(&Element::C) { bond_energy * 0.5 } else { 0.0 },
..Default::default()
},
phase: PhaseProps {
state,
viscosity,
relaxation_time: if state == PhaseState::Liquid {
viscosity_to_tau(viscosity)
} else {
1.0
},
surface_tension: if state == PhaseState::Liquid {
if mol.has_hydrogen_bonds() { 0.072 } else { 0.025 } } else {
0.0
},
..Default::default()
},
optical,
..Default::default()
}
}
fn estimate_molecular_melting(mol: &Molecule) -> f32 {
if let Some(elem) = pure_element_molecule(mol) {
return elem.melting_point_pure();
}
let mass = mol.molecular_mass();
let has_h_bonds = mol.has_hydrogen_bonds();
if has_h_bonds {
-150.0 + mass.sqrt() * 35.0
} else {
-200.0 + mass.sqrt() * 30.0
}
}
fn estimate_molecular_boiling(mol: &Molecule, mp_c: f32) -> f32 {
if let Some(elem) = pure_element_molecule(mol) {
return elem.boiling_point_pure();
}
let mass = mol.molecular_mass();
let has_h_bonds = mol.has_hydrogen_bonds();
if has_h_bonds {
mp_c + 100.0 + mass * 0.5
} else {
mp_c + 50.0 + mass * 0.3
}
}
fn pure_element_molecule(mol: &Molecule) -> Option<Element> {
if mol.atoms.is_empty() {
return None;
}
let first = mol.atoms[0];
if mol.atoms.iter().all(|&a| a == first) {
Some(first)
} else {
None
}
}
fn estimate_molecular_density(mol: &Molecule, state: PhaseState) -> f32 {
let mass = mol.molecular_mass();
match state {
PhaseState::Liquid => {
((mass / 18.0) * 800.0 + 200.0).clamp(500.0, 2000.0)
}
PhaseState::Gas => {
(101325.0 * mass * 1e-3) / (8.314 * 300.0)
}
_ => {
mass * 50.0
}
}
}
fn estimate_molecular_specific_heat(mol: &Molecule, state: PhaseState) -> f32 {
let mass = mol.molecular_mass();
let has_oh = mol.atoms.contains(&Element::O) && mol.atoms.contains(&Element::H);
let n_atoms = mol.atoms.len();
match state {
PhaseState::Liquid => {
if has_oh && mass < 30.0 {
4186.0
} else if has_oh {
2500.0
} else {
2000.0
}
}
PhaseState::Gas => {
let dof = if n_atoms == 1 {
3.0 } else if n_atoms == 2 {
5.0 } else {
6.0 };
(dof + 2.0) / 2.0 * 8.314 / mass * 1000.0
}
_ => {
let n = n_atoms as f32;
(25.0 * n) / mass * 1000.0
}
}
}
fn compile_crystal(c: &Crystal) -> MaterialDef {
let density = estimate_crystal_density(c);
let melting = estimate_melting_point(c);
let boiling = estimate_crystal_boiling(c, melting);
let conductivity = estimate_conductivity(c);
let specific_heat = estimate_specific_heat(c);
let yield_str = estimate_yield_strength(c);
let is_metal = c.base.is_metal();
let emissivity = estimate_emissivity(is_metal, 0.3);
let ior = estimate_ior(density, is_metal);
let mut optical = estimate_optical(
&Substance::Crystalline(c.clone()),
PhaseState::Solid,
is_metal,
density,
);
optical.emissivity = emissivity;
optical.refractive_index = ior;
MaterialDef {
name: c.name.clone(),
structural: StructuralProps {
density,
yield_strength: yield_str,
impact_toughness: yield_str * 0.1,
plasticity: if is_metal { 0.3 } else { 0.05 },
..Default::default()
},
thermal: ThermalProps {
specific_heat,
thermal_conductivity: conductivity,
melting_point: Some(melting),
vaporisation_point: Some(boiling),
emission_onset_temperature: if is_metal { Some(melting * 0.4) } else { None },
..Default::default()
},
phase: PhaseProps {
state: PhaseState::Solid,
repose_angle: 90.0,
..Default::default()
},
optical,
..Default::default()
}
}
fn estimate_crystal_boiling(c: &Crystal, melting: f32) -> f32 {
match classify_crystal(c) {
CrystalClass::Metallic => {
let base_bp = c.base.boiling_point_pure();
let solute_shift: f32 = c.solutes.iter()
.map(|(elem, frac)| {
(elem.boiling_point_pure() - base_bp) * frac * 0.5
})
.sum();
base_bp + solute_shift
}
_ => {
melting + (melting + 273.15).abs() * 0.5
}
}
}
fn compile_polymer(p: &PolymerChain) -> MaterialDef {
let monomer_mass = p.monomer.molecular_mass();
let density = 900.0 + p.cross_link_density * 500.0;
let melting = estimate_polymer_tg(p);
let optical = estimate_optical(
&Substance::Polymer(p.clone()),
PhaseState::Solid,
false,
density,
);
MaterialDef {
name: p.name.clone(),
structural: StructuralProps {
density,
yield_strength: 20e6 + p.cross_link_density * 80e6, plasticity: 0.5 - p.cross_link_density * 0.3,
..Default::default()
},
thermal: ThermalProps {
specific_heat: 1500.0,
thermal_conductivity: 0.2,
melting_point: Some(melting),
ignition_point: Some(melting + 100.0),
combustion_energy: monomer_mass * p.chain_length as f32 * 10.0,
..Default::default()
},
phase: PhaseProps {
state: PhaseState::Solid,
..Default::default()
},
optical,
..Default::default()
}
}
fn estimate_polymer_tg(p: &PolymerChain) -> f32 {
let base_tg = -120.0 + p.monomer.molecular_mass() * 2.0;
let cross_link_boost = p.cross_link_density * 200.0;
base_tg + cross_link_boost
}
fn compile_amorphous(name: &str, comp: &[(Element, f32)], density: f32) -> MaterialDef {
let is_metal = comp.iter().any(|(e, f)| e.is_metal() && *f > 0.3);
let melting: f32 = comp.iter()
.map(|(e, f)| e.melting_point_pure() * f)
.sum();
let yield_strength = if is_metal {
200e6
} else {
estimate_amorphous_yield(comp, density)
};
let emissivity = estimate_emissivity(is_metal, 0.5);
let ior = estimate_ior(density, is_metal);
let mut optical = estimate_optical(
&Substance::Amorphous {
name: name.into(),
composition: comp.to_vec(),
density,
},
PhaseState::Solid,
is_metal,
density,
);
optical.emissivity = emissivity;
optical.refractive_index = ior;
if !is_metal {
optical.base_color = estimate_amorphous_color(comp);
}
MaterialDef {
name: name.into(),
structural: StructuralProps {
density,
yield_strength,
..Default::default()
},
thermal: ThermalProps {
specific_heat: estimate_specific_heat_from_mass(comp),
thermal_conductivity: if is_metal { 30.0 } else { 1.0 },
melting_point: Some(melting),
..Default::default()
},
phase: PhaseProps {
state: PhaseState::Solid,
..Default::default()
},
optical,
..Default::default()
}
}
fn estimate_crystal_density(c: &Crystal) -> f32 {
let mass = c.base.atomic_mass(); let r_m = c.base.atomic_radius() * 1e-12;
let a = r_m * match c.lattice {
LatticeType::BCC => 4.0 / 3.0_f32.sqrt(), LatticeType::FCC => 2.0 * 2.0_f32.sqrt(), LatticeType::HCP => 2.0, LatticeType::BCT => 4.0 / 3.0_f32.sqrt(), LatticeType::Diamond => 8.0 / 3.0_f32.sqrt(), LatticeType::Ionic => 2.5, };
let atoms_per_cell: f32 = match c.lattice {
LatticeType::BCC => 2.0,
LatticeType::FCC => 4.0,
LatticeType::HCP => 6.0, LatticeType::BCT => 2.0,
LatticeType::Diamond => 8.0,
LatticeType::Ionic => 4.0,
};
let v_cell = a * a * a; let avogadro: f32 = 6.022e23;
let base_density = (atoms_per_cell * mass * 1e-3) / (v_cell * avogadro);
let solute_frac: f32 = c.solutes.iter().map(|(_, f)| f).sum();
let solute_mass: f32 = c.solutes.iter()
.map(|(e, f)| e.atomic_mass() * f)
.sum::<f32>();
let effective_mass = mass * (1.0 - solute_frac) + solute_mass;
base_density * effective_mass / mass
}
fn estimate_melting_point(c: &Crystal) -> f32 {
match classify_crystal(c) {
CrystalClass::Metallic => {
let base_tm = c.base.melting_point_pure();
let solute_effect: f32 = c.solutes.iter()
.map(|(elem, frac)| {
let depression_factor = if matches!(elem, Element::C | Element::N | Element::B) {
-5000.0 } else {
let delta_tm = elem.melting_point_pure() - base_tm;
delta_tm };
depression_factor * frac
})
.sum();
base_tm + solute_effect
}
CrystalClass::Covalent => {
let bond_e = c.base.cohesive_energy();
let coord = c.lattice.coordination_number() as f32;
bond_e * coord * 80.0 - 273.15
}
CrystalClass::Ionic => {
c.base.melting_point_pure()
}
}
}
fn estimate_conductivity(c: &Crystal) -> f32 {
if let Some(rho_e) = c.base.electrical_resistivity() {
let rho_si = rho_e * 1e-8; let lorenz = 2.44e-8_f32; let t = 300.0_f32; return lorenz * t / rho_si; }
if c.base.is_metal() {
let ce = c.cohesive_energy();
let en = c.base.electronegativity().max(0.5);
(ce / en) * 40.0
} else {
if c.lattice == LatticeType::Diamond && c.base == Element::C {
2200.0 } else {
2.0
}
}
}
fn estimate_specific_heat(c: &Crystal) -> f32 {
let mass = c.base.atomic_mass();
dulong_petit(mass)
}
fn dulong_petit(molar_mass: f32) -> f32 {
25.0 / molar_mass * 1000.0 }
fn estimate_yield_strength(c: &Crystal) -> f32 {
let ce = c.cohesive_energy();
let shear_mod = ce * 15e9;
let peierls = shear_mod / 30.0;
let solute_hardening: f32 = c.solutes.iter()
.map(|(_, frac)| 500e6 * frac.sqrt())
.sum();
peierls + solute_hardening
}
fn estimate_emissivity(is_metal: bool, roughness: f32) -> f32 {
if is_metal {
0.05 + roughness * 0.25
} else {
0.85 + roughness * 0.10
}
}
fn estimate_ior(density: f32, is_metal: bool) -> f32 {
if is_metal {
2.0 + density * 0.00005
} else {
let x = (density / 3000.0).sqrt();
1.0 + x * 1.2
}
}
fn estimate_viscosity(melting_point_c: f32) -> f32 {
let tm_normalized = (melting_point_c + 273.0) / 373.0;
(0.001 * (tm_normalized * 3.0).exp()).min(100000.0)
}
fn estimate_optical(
sub: &Substance,
state: PhaseState,
is_metal: bool,
density: f32,
) -> OpticalProps {
match sub {
Substance::Crystalline(c) if is_metal => {
OpticalProps {
base_color: c.base.base_color(),
metallic: 1.0,
roughness: 0.3,
opacity: 1.0,
refractive_index: 2.5,
emissivity: 0.3,
..Default::default()
}
}
Substance::Molecular(mol) => {
let is_water = mol.name == "water";
if is_water {
OpticalProps {
base_color: [0.2, 0.4, 0.8, 0.6],
metallic: 0.0,
roughness: 0.05,
opacity: 0.3,
refractive_index: 1.33,
absorption_color: [0.6, 0.8, 1.0],
absorption_density: 0.1,
..Default::default()
}
} else if state == PhaseState::Gas {
OpticalProps {
base_color: [1.0, 1.0, 1.0, 0.0],
metallic: 0.0,
roughness: 1.0,
opacity: 0.0,
refractive_index: 1.0,
..Default::default()
}
} else {
let ior = estimate_ior(density, false);
OpticalProps {
base_color: [0.8, 0.8, 0.8, 0.9],
metallic: 0.0,
roughness: 0.5,
opacity: 0.8,
refractive_index: ior.min(2.0),
..Default::default()
}
}
}
Substance::Polymer(_p) => {
OpticalProps {
base_color: [0.9, 0.9, 0.85, 1.0],
metallic: 0.0,
roughness: 0.6,
opacity: 1.0,
refractive_index: 1.5,
..Default::default()
}
}
Substance::Crystalline(_) | Substance::Amorphous { .. } => {
OpticalProps {
base_color: [0.6, 0.6, 0.55, 1.0],
metallic: 0.0,
roughness: 0.7,
opacity: 1.0,
refractive_index: estimate_ior(density, false),
..Default::default()
}
}
}
}
fn estimate_specific_heat_from_mass(comp: &[(Element, f32)]) -> f32 {
let avg_mass: f32 = comp.iter()
.map(|(e, f)| e.atomic_mass() * f)
.sum();
if avg_mass > 0.0 {
dulong_petit(avg_mass)
} else {
1000.0
}
}
fn adjust_for_phase(mat: &mut MaterialDef, sub: &Substance, target_phase: PhaseState) {
let natural_phase = mat.phase.state;
if natural_phase == target_phase {
return;
}
mat.phase.state = target_phase;
match target_phase {
PhaseState::Gas => {
let molar_mass = estimate_molar_mass(sub);
mat.structural.density = (101325.0 * molar_mass) / (8314.0 * 300.0); mat.phase.relaxation_time = 0.6; mat.phase.viscosity = 1e-5;
mat.structural.yield_strength = 0.0;
mat.optical.opacity = 0.0;
mat.optical.metallic = 0.0;
mat.optical.roughness = 1.0;
mat.optical.base_color[3] = 0.1; }
PhaseState::Liquid => {
if natural_phase == PhaseState::Solid {
mat.structural.density *= 0.9;
}
mat.phase.viscosity = estimate_liquid_viscosity_for_phase(mat, natural_phase);
mat.phase.relaxation_time = viscosity_to_tau(mat.phase.viscosity);
mat.structural.yield_strength = 0.0;
mat.phase.surface_tension = if mat.structural.density > 5000.0 {
1.5 } else if mat.structural.density > 2000.0 {
0.3 } else {
0.05 };
let effective_temp_k = if natural_phase == PhaseState::Solid && mat.structural.density > 2000.0 {
mat.thermal.melting_point.unwrap_or(1200.0).max(1200.0) + 273.15
} else {
mat.thermal.melting_point.unwrap_or(0.0) + 273.15
};
if effective_temp_k > 773.15 {
mat.optical.emissivity = 0.9;
mat.optical.emission_intensity = (effective_temp_k / 1000.0).powi(4) * 0.1;
mat.optical.emission_color = planck_color(effective_temp_k);
mat.optical.base_color = incandescent_color(effective_temp_k);
}
}
PhaseState::Granular => {
mat.structural.density *= 0.6;
mat.phase.relaxation_time = 3.0; mat.phase.repose_angle = 35.0; mat.structural.yield_strength = 0.0; mat.hydraulic.porosity = 0.4; mat.hydraulic.permeability = 0.01;
mat.optical.roughness = 0.95; }
PhaseState::Solid => {
if natural_phase == PhaseState::Liquid {
mat.structural.density *= 0.92; mat.phase.relaxation_time = 50.0;
mat.phase.viscosity = 0.0;
mat.phase.repose_angle = 90.0;
}
}
}
}
fn estimate_molar_mass(sub: &Substance) -> f32 {
match sub {
Substance::Molecular(mol) => mol.molecular_mass(),
Substance::Crystalline(c) => c.base.atomic_mass(),
Substance::Polymer(p) => p.monomer.molecular_mass(),
Substance::Amorphous { composition, .. } => {
let total: f32 = composition.iter().map(|(_, f)| f).sum();
if total > 0.0 {
composition.iter().map(|(e, f)| e.atomic_mass() * f).sum::<f32>() / total
} else {
30.0 }
}
}
}
fn estimate_liquid_viscosity_for_phase(mat: &MaterialDef, natural_phase: PhaseState) -> f32 {
if natural_phase == PhaseState::Solid {
let density = mat.structural.density;
if density > 2000.0 { 100.0 } else if density > 1000.0 { 1.0 } else { 0.01 } } else if let Some(mp) = mat.thermal.melting_point {
estimate_viscosity(mp)
} else {
0.01
}
}
fn viscosity_to_tau(viscosity: f32) -> f32 {
(0.8 + viscosity.log10().max(-3.0).min(5.0) * 0.5).clamp(0.55, 10.0)
}
fn planck_color(temp_k: f32) -> [f32; 3] {
let t = (temp_k / 1000.0).clamp(1.0, 10.0);
let r = (1.0 - (-0.5 * (t - 1.0)).exp()).min(1.0);
let g = (1.0 - (-0.3 * (t - 2.0)).exp()).max(0.0).min(1.0);
let b = (1.0 - (-0.2 * (t - 4.0)).exp()).max(0.0).min(1.0);
[r, g, b]
}
fn incandescent_color(temp_k: f32) -> [f32; 4] {
let [r, g, b] = planck_color(temp_k);
[r, g * 0.6, b * 0.2, 1.0] }
fn estimate_amorphous_yield(comp: &[(Element, f32)], density: f32) -> f32 {
let si_frac: f32 = comp.iter().filter(|(e, _)| *e == Element::Si).map(|(_, f)| f).sum();
let fe_frac: f32 = comp.iter().filter(|(e, _)| *e == Element::Fe).map(|(_, f)| f).sum();
let c_frac: f32 = comp.iter().filter(|(e, _)| *e == Element::C).map(|(_, f)| f).sum();
let base = density * 30.0; let silica_boost = si_frac * 300e6; let iron_boost = fe_frac * 200e6; let organic_penalty = c_frac * 100e6;
(base + silica_boost + iron_boost - organic_penalty).max(1e6) }
fn estimate_amorphous_color(comp: &[(Element, f32)]) -> [f32; 4] {
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
let mut total_weight = 0.0f32;
for &(elem, frac) in comp {
let (er, eg, eb) = oxide_pigment_color(elem);
r += er * frac;
g += eg * frac;
b += eb * frac;
total_weight += frac;
}
if total_weight > 0.0 {
[r / total_weight, g / total_weight, b / total_weight, 1.0]
} else {
[0.5, 0.5, 0.5, 1.0]
}
}
fn oxide_pigment_color(elem: Element) -> (f32, f32, f32) {
match elem {
Element::Fe => (0.60, 0.30, 0.20), Element::Al => (0.85, 0.85, 0.82), Element::Si => (0.65, 0.63, 0.60), Element::Ca => (0.90, 0.88, 0.85), Element::Mg => (0.88, 0.88, 0.85), Element::Ti => (0.95, 0.95, 0.93), Element::Cr => (0.30, 0.50, 0.25), Element::Cu => (0.20, 0.35, 0.25), Element::Mn => (0.35, 0.25, 0.25), Element::K => (0.80, 0.78, 0.75), Element::Na => (0.82, 0.80, 0.78), Element::O => (0.75, 0.75, 0.73), Element::C => (0.20, 0.18, 0.15), Element::H => (0.80, 0.80, 0.80), Element::N => (0.60, 0.55, 0.45), Element::P => (0.70, 0.65, 0.55), Element::S => (0.80, 0.75, 0.30), _ => {
let [er, eg, eb, _] = elem.base_color();
(er, eg, eb)
}
}
}