use std::fmt::Write as _;
use std::{fs, io, path::Path, str::FromStr};
use crate::api::materials::catalog::MaterialCatalog;
use crate::api::objects::SceneObject;
use crate::api::types::CameraDesc;
use crate::core::engine::rendering::effects::volumetric_effects::medium::VolumetricMedium;
use crate::core::engine::rendering::raytracing::{
AreaLight, Camera, DirectionalLight, Material, Scene, Sphere, Triangle, Vec3,
};
#[derive(Debug, Clone)]
pub struct SceneBuilder {
spheres: Vec<Sphere>,
triangles: Vec<Triangle>,
sun_direction: Vec3,
sun_color: Vec3,
sun_intensity: f64,
sun_angular_radius: f64,
area_lights: Vec<AreaLight>,
sky_top: Vec3,
sky_bottom: Vec3,
exposure: f64,
volume: VolumetricMedium,
camera: CameraDesc,
}
impl Default for SceneBuilder {
fn default() -> Self {
Self::new()
}
}
impl SceneBuilder {
pub fn new() -> Self {
Self {
spheres: Vec::new(),
triangles: Vec::new(),
sun_direction: Vec3::new(-0.65, -0.35, -1.0).normalize(),
sun_color: Vec3::new(1.0, 0.96, 0.90),
sun_intensity: 1.45,
sun_angular_radius: 0.03,
area_lights: Vec::new(),
sky_top: Vec3::new(0.015, 0.020, 0.050),
sky_bottom: Vec3::new(0.001, 0.001, 0.006),
exposure: 1.45,
volume: VolumetricMedium::cinematic_nebula().with_density_multiplier(0.9),
camera: CameraDesc::default(),
}
}
pub fn add_sphere(mut self, center: Vec3, radius: f64, material: Material) -> Self {
self.spheres.push(Sphere {
center,
radius: radius.max(0.01),
material,
});
self
}
pub fn add_sphere_named(self, center: Vec3, radius: f64, material_name: &str) -> Self {
let material = MaterialCatalog.by_name(material_name);
self.add_sphere(center, radius, material)
}
pub fn add_object(mut self, object: SceneObject) -> Self {
let (spheres, triangles) = object.into_primitives();
self.spheres.extend(spheres);
self.triangles.extend(triangles);
self
}
pub fn add_triangle(mut self, a: Vec3, b: Vec3, c: Vec3, material: Material) -> Self {
self.triangles.push(Triangle::flat(a, b, c, material));
self
}
pub fn add_mesh(
mut self,
mesh: &crate::core::engine::rendering::mesh::asset::MeshAsset,
translation: Vec3,
scale: f64,
material: Material,
) -> Self {
self.triangles
.extend(mesh.to_triangles(translation, scale, material));
self
}
pub fn sun_direction(mut self, dir: [f64; 3]) -> Self {
self.sun_direction = Vec3::new(dir[0], dir[1], dir[2]).normalize();
self
}
pub fn sun_color(mut self, rgb: [f64; 3]) -> Self {
self.sun_color = Vec3::new(rgb[0], rgb[1], rgb[2]);
self
}
pub fn sun_intensity(mut self, intensity: f64) -> Self {
self.sun_intensity = intensity.max(0.0);
self
}
pub fn add_area_light(
mut self,
position: [f64; 3],
color: [f64; 3],
intensity: f64,
size: [f64; 2],
) -> Self {
self.area_lights.push(AreaLight {
position: Vec3::new(position[0], position[1], position[2]),
u: Vec3::new(size[0], 0.0, 0.0),
v: Vec3::new(0.0, 0.0, size[1]),
color: Vec3::new(color[0], color[1], color[2]),
intensity: intensity.max(0.0),
});
self
}
pub fn sky(mut self, top: [f64; 3], bottom: [f64; 3]) -> Self {
self.sky_top = Vec3::new(top[0], top[1], top[2]);
self.sky_bottom = Vec3::new(bottom[0], bottom[1], bottom[2]);
self
}
pub fn exposure(mut self, exposure: f64) -> Self {
self.exposure = exposure.max(0.01);
self
}
pub fn with_dense_volume(mut self) -> Self {
self.volume = VolumetricMedium::cinematic_nebula().with_density_multiplier(1.8);
self
}
pub fn with_vacuum(mut self) -> Self {
self.volume = VolumetricMedium::vacuum();
self
}
pub fn with_volume(mut self, medium: VolumetricMedium) -> Self {
self.volume = medium;
self
}
pub fn with_camera(mut self, desc: CameraDesc) -> Self {
self.camera = desc;
self
}
pub fn camera_position(mut self, eye: [f64; 3], target: [f64; 3]) -> Self {
self.camera.eye = eye;
self.camera.target = target;
self
}
pub fn camera_fov(mut self, degrees: f64) -> Self {
self.camera.fov_degrees = degrees.clamp(10.0, 120.0);
self
}
pub fn camera_aperture(mut self, aperture: f64) -> Self {
self.camera.aperture = aperture.max(0.0);
self
}
pub fn auto_frame(mut self) -> Self {
if self.spheres.is_empty() && self.triangles.is_empty() {
return self;
}
let sphere_center_sum = self.spheres.iter().fold(Vec3::ZERO, |a, s| a + s.center);
let triangle_center_sum = self
.triangles
.iter()
.fold(Vec3::ZERO, |a, t| a + (t.a + t.b + t.c) / 3.0);
let sample_count = (self.spheres.len() + self.triangles.len()) as f64;
let center = (sphere_center_sum + triangle_center_sum) / sample_count.max(1.0);
let sphere_extent = self
.spheres
.iter()
.map(|s| (s.center - center).length() + s.radius)
.fold(0.0_f64, f64::max);
let triangle_extent = self
.triangles
.iter()
.map(|t| {
(t.a - center)
.length()
.max((t.b - center).length())
.max((t.c - center).length())
})
.fold(0.0_f64, f64::max);
let extent = sphere_extent.max(triangle_extent).max(1.0);
let dist = extent * 2.8;
self.camera.eye = [
center.x + dist * 0.7,
center.y + dist * 0.45,
center.z + dist * 0.7,
];
self.camera.target = [center.x, center.y, center.z];
self
}
pub fn build(self, aspect_ratio: f64) -> (Scene, Camera) {
let scene = Scene {
objects: self.spheres,
triangles: self.triangles,
sun: DirectionalLight {
direction: self.sun_direction,
color: self.sun_color,
intensity: self.sun_intensity,
angular_radius: self.sun_angular_radius,
},
area_lights: self.area_lights,
sky_top: self.sky_top,
sky_bottom: self.sky_bottom,
exposure: self.exposure,
volume: self.volume,
hdri: None,
solar_elevation: 0.48,
};
let eye = Vec3::new(self.camera.eye[0], self.camera.eye[1], self.camera.eye[2]);
let target = Vec3::new(
self.camera.target[0],
self.camera.target[1],
self.camera.target[2],
);
let mut camera = Camera::look_at(
eye,
target,
Vec3::new(0.0, 1.0, 0.0),
self.camera.fov_degrees,
aspect_ratio,
);
if self.camera.aperture > 0.0 {
camera = camera.with_physical_lens(self.camera.aperture, 0.0, Vec3::ZERO);
}
(scene, camera)
}
}
#[derive(Debug, Clone)]
pub struct SphereEntry {
pub position: [f64; 3],
pub radius: f64,
pub material_name: Option<String>,
pub albedo: [f64; 3],
pub roughness: f64,
pub metallic: f64,
pub emission: f64,
}
impl Default for SphereEntry {
fn default() -> Self {
Self {
position: [0.0; 3],
radius: 1.0,
material_name: None,
albedo: [0.8, 0.8, 0.8],
roughness: 0.5,
metallic: 0.0,
emission: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct TriangleEntry {
pub a: [f64; 3],
pub b: [f64; 3],
pub c: [f64; 3],
pub material_name: Option<String>,
pub albedo: [f64; 3],
pub roughness: f64,
pub metallic: f64,
pub emission: f64,
}
impl Default for TriangleEntry {
fn default() -> Self {
Self {
a: [0.0; 3],
b: [0.0; 3],
c: [0.0; 3],
material_name: None,
albedo: [0.8, 0.8, 0.8],
roughness: 0.5,
metallic: 0.0,
emission: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct AreaLightEntry {
pub position: [f64; 3],
pub color: [f64; 3],
pub intensity: f64,
pub size: [f64; 2],
}
impl Default for AreaLightEntry {
fn default() -> Self {
Self {
position: [0.0, 5.0, 0.0],
color: [1.0, 1.0, 1.0],
intensity: 1.0,
size: [2.0, 2.0],
}
}
}
#[derive(Debug, Clone)]
pub struct SceneDescriptor {
pub camera: CameraDesc,
pub sun_direction: [f64; 3],
pub sun_color: [f64; 3],
pub sun_intensity: f64,
pub sky_top: [f64; 3],
pub sky_bottom: [f64; 3],
pub exposure: f64,
pub spheres: Vec<SphereEntry>,
pub triangles: Vec<TriangleEntry>,
pub area_lights: Vec<AreaLightEntry>,
}
impl Default for SceneDescriptor {
fn default() -> Self {
Self {
camera: CameraDesc::default(),
sun_direction: [-0.65, -0.35, -1.0],
sun_color: [1.0, 0.96, 0.90],
sun_intensity: 1.45,
sky_top: [0.015, 0.020, 0.050],
sky_bottom: [0.001, 0.001, 0.006],
exposure: 1.45,
spheres: Vec::new(),
triangles: Vec::new(),
area_lights: Vec::new(),
}
}
}
impl SceneDescriptor {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let path_ref = path.as_ref();
let metadata = fs::metadata(path_ref)?;
let size = metadata.len();
if size > MAX_SCENE_FILE_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("scene file size {size} exceeds limit {MAX_SCENE_FILE_SIZE}"),
));
}
let text = fs::read_to_string(path_ref)?;
Self::parse(&text).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
if let Some(parent) = path.as_ref().parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
fs::write(path, self.serialize())
}
pub fn parse(text: &str) -> Result<Self, String> {
let mut desc = SceneDescriptor::default();
let line_count = text.bytes().filter(|&b| b == b'\n').count();
let estimated = line_count.saturating_sub(8) / 2;
if estimated > 0 {
desc.spheres.reserve(estimated.min(MAX_SCENE_SPHERES));
}
for (zero_based, raw_line) in text.lines().enumerate() {
let line_number = zero_based + 1;
let stripped = if zero_based == 0 {
raw_line.strip_prefix('\u{feff}').unwrap_or(raw_line)
} else {
raw_line
};
let line = stripped.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (keyword, rest) = line
.split_once(' ')
.map(|(k, r)| (k, r.trim()))
.unwrap_or((line, ""));
match keyword {
"version" => {}
"camera" => {
let kv = KvMap::parse(rest, line_number)?;
if let Some(v) = kv.get("eye") {
desc.camera.eye = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("target") {
desc.camera.target = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("fov") {
let value = parse_f64_val(v, line_number)?;
if value <= 0.0 || value >= 180.0 {
return Err(format!(
"line {line_number}: fov must be in (0, 180), got {value}"
));
}
desc.camera.fov_degrees = value;
}
if let Some(v) = kv.get("aperture") {
let value = parse_f64_val(v, line_number)?;
if value < 0.0 {
return Err(format!(
"line {line_number}: aperture must be non-negative, got {value}"
));
}
desc.camera.aperture = value;
}
}
"sun" => {
let kv = KvMap::parse(rest, line_number)?;
if let Some(v) = kv.get("dir") {
desc.sun_direction = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("intensity") {
let value = parse_f64_val(v, line_number)?;
if value < 0.0 {
return Err(format!(
"line {line_number}: sun intensity must be non-negative, got {value}"
));
}
desc.sun_intensity = value;
}
if let Some(v) = kv.get("color") {
desc.sun_color = parse_f64_vec3(v, line_number)?;
}
}
"sky" => {
let kv = KvMap::parse(rest, line_number)?;
if let Some(v) = kv.get("top") {
desc.sky_top = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("bottom") {
desc.sky_bottom = parse_f64_vec3(v, line_number)?;
}
}
"exposure" => {
let value = parse_f64_val(rest, line_number)?;
if value <= 0.0 {
return Err(format!(
"line {line_number}: exposure must be positive, got {value}"
));
}
desc.exposure = value;
}
"sphere" => {
if desc.spheres.len() >= MAX_SCENE_SPHERES {
return Err(format!(
"line {line_number}: sphere count exceeds limit {MAX_SCENE_SPHERES}"
));
}
let kv = KvMap::parse(rest, line_number)?;
let mut entry = SphereEntry::default();
if let Some(v) = kv.get("pos") {
entry.position = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("radius") {
let value = parse_f64_val(v, line_number)?;
if value <= 0.0 {
return Err(format!(
"line {line_number}: sphere radius must be positive, got {value}"
));
}
entry.radius = value;
}
if let Some(v) = kv.get("material") {
validate_material_name(v, line_number)?;
entry.material_name = Some(v.to_string());
}
if let Some(v) = kv.get("albedo") {
entry.albedo = parse_unit_vec3(v, line_number)?;
}
if let Some(v) = kv.get("roughness") {
entry.roughness = parse_unit_scalar(v, line_number)?;
}
if let Some(v) = kv.get("metallic") {
entry.metallic = parse_unit_scalar(v, line_number)?;
}
if let Some(v) = kv.get("emission") {
let value = parse_f64_val(v, line_number)?;
if value < 0.0 {
return Err(format!(
"line {line_number}: emission must be non-negative, got {value}"
));
}
entry.emission = value;
}
desc.spheres.push(entry);
}
"triangle" => {
if desc.triangles.len() >= MAX_SCENE_TRIANGLES {
return Err(format!(
"line {line_number}: triangle count exceeds limit {MAX_SCENE_TRIANGLES}"
));
}
let kv = KvMap::parse(rest, line_number)?;
let mut entry = TriangleEntry::default();
if let Some(v) = kv.get("a") {
entry.a = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("b") {
entry.b = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("c") {
entry.c = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("material") {
validate_material_name(v, line_number)?;
entry.material_name = Some(v.to_string());
}
if let Some(v) = kv.get("albedo") {
entry.albedo = parse_unit_vec3(v, line_number)?;
}
if let Some(v) = kv.get("roughness") {
entry.roughness = parse_unit_scalar(v, line_number)?;
}
if let Some(v) = kv.get("metallic") {
entry.metallic = parse_unit_scalar(v, line_number)?;
}
if let Some(v) = kv.get("emission") {
let value = parse_f64_val(v, line_number)?;
if value < 0.0 {
return Err(format!(
"line {line_number}: emission must be non-negative, got {value}"
));
}
entry.emission = value;
}
desc.triangles.push(entry);
}
"area_light" => {
if desc.area_lights.len() >= MAX_SCENE_AREA_LIGHTS {
return Err(format!(
"line {line_number}: area_light count exceeds limit {MAX_SCENE_AREA_LIGHTS}"
));
}
let kv = KvMap::parse(rest, line_number)?;
let mut entry = AreaLightEntry::default();
if let Some(v) = kv.get("pos") {
entry.position = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("color") {
entry.color = parse_f64_vec3(v, line_number)?;
}
if let Some(v) = kv.get("intensity") {
let value = parse_f64_val(v, line_number)?;
if value < 0.0 {
return Err(format!(
"line {line_number}: area_light intensity must be non-negative, got {value}"
));
}
entry.intensity = value;
}
if let Some(v) = kv.get("size") {
let size = parse_f64_vec2(v, line_number)?;
if size[0] <= 0.0 || size[1] <= 0.0 {
return Err(format!(
"line {line_number}: area_light size must be positive, got {},{}",
size[0], size[1]
));
}
entry.size = size;
}
desc.area_lights.push(entry);
}
other => {
return Err(format!("line {line_number}: unknown keyword '{other}'"));
}
}
}
Ok(desc)
}
pub fn serialize(&self) -> String {
let estimated = 128
+ self.spheres.len() * 144
+ self.triangles.len() * 224
+ self.area_lights.len() * 144;
let mut s = String::with_capacity(estimated);
s.push_str("version 1\n");
s.push_str("camera eye=");
fmt3_into(&mut s, self.camera.eye);
s.push_str(" target=");
fmt3_into(&mut s, self.camera.target);
let _ = writeln!(
&mut s,
" fov={:.4} aperture={:.6}",
self.camera.fov_degrees, self.camera.aperture,
);
s.push_str("sun dir=");
fmt3_into(&mut s, self.sun_direction);
let _ = write!(&mut s, " intensity={:.4} color=", self.sun_intensity);
fmt3_into(&mut s, self.sun_color);
s.push('\n');
s.push_str("sky top=");
fmt3_into(&mut s, self.sky_top);
s.push_str(" bottom=");
fmt3_into(&mut s, self.sky_bottom);
s.push('\n');
let _ = writeln!(&mut s, "exposure {:.4}", self.exposure);
for sphere in &self.spheres {
s.push_str("sphere pos=");
fmt3_into(&mut s, sphere.position);
let _ = write!(&mut s, " radius={:.4}", sphere.radius);
if let Some(mat) = &sphere.material_name {
s.push_str(" material=");
s.push_str(mat);
s.push('\n');
} else {
s.push_str(" albedo=");
fmt3_into(&mut s, sphere.albedo);
let _ = writeln!(
&mut s,
" roughness={:.4} metallic={:.4} emission={:.4}",
sphere.roughness, sphere.metallic, sphere.emission,
);
}
}
for triangle in &self.triangles {
s.push_str("triangle a=");
fmt3_into(&mut s, triangle.a);
s.push_str(" b=");
fmt3_into(&mut s, triangle.b);
s.push_str(" c=");
fmt3_into(&mut s, triangle.c);
if let Some(mat) = &triangle.material_name {
s.push_str(" material=");
s.push_str(mat);
s.push('\n');
} else {
s.push_str(" albedo=");
fmt3_into(&mut s, triangle.albedo);
let _ = writeln!(
&mut s,
" roughness={:.4} metallic={:.4} emission={:.4}",
triangle.roughness, triangle.metallic, triangle.emission,
);
}
}
for light in &self.area_lights {
s.push_str("area_light pos=");
fmt3_into(&mut s, light.position);
s.push_str(" color=");
fmt3_into(&mut s, light.color);
let _ = write!(&mut s, " intensity={:.4} size=", light.intensity);
fmt2_into(&mut s, light.size);
s.push('\n');
}
s
}
pub fn into_builder(self) -> SceneBuilder {
let mut builder = SceneBuilder::new()
.sun_direction(self.sun_direction)
.sun_color(self.sun_color)
.sun_intensity(self.sun_intensity)
.sky(self.sky_top, self.sky_bottom)
.exposure(self.exposure)
.with_camera(self.camera);
for entry in self.spheres {
let material = if let Some(name) = &entry.material_name {
MaterialCatalog.by_name(name)
} else {
Material::new(
Vec3::new(entry.albedo[0], entry.albedo[1], entry.albedo[2]),
entry.roughness,
entry.metallic,
entry.metallic.clamp(0.0, 1.0),
Vec3::new(entry.emission, entry.emission, entry.emission),
)
};
builder = builder.add_sphere(
Vec3::new(entry.position[0], entry.position[1], entry.position[2]),
entry.radius,
material,
);
}
for entry in self.triangles {
let material = if let Some(name) = &entry.material_name {
MaterialCatalog.by_name(name)
} else {
Material::new(
Vec3::new(entry.albedo[0], entry.albedo[1], entry.albedo[2]),
entry.roughness,
entry.metallic,
entry.metallic.clamp(0.0, 1.0),
Vec3::new(entry.emission, entry.emission, entry.emission),
)
};
builder = builder.add_triangle(
Vec3::new(entry.a[0], entry.a[1], entry.a[2]),
Vec3::new(entry.b[0], entry.b[1], entry.b[2]),
Vec3::new(entry.c[0], entry.c[1], entry.c[2]),
material,
);
}
for light in self.area_lights {
builder =
builder.add_area_light(light.position, light.color, light.intensity, light.size);
}
builder
}
}
const KVMAP_CAPACITY: usize = 16;
struct KvMap<'a> {
buf: [(&'a str, &'a str); KVMAP_CAPACITY],
len: usize,
}
impl<'a> KvMap<'a> {
fn parse(s: &'a str, line_number: usize) -> Result<Self, String> {
let mut buf: [(&'a str, &'a str); KVMAP_CAPACITY] = [("", ""); KVMAP_CAPACITY];
let mut len = 0usize;
for token in s.split_whitespace() {
let (key, value) = token.split_once('=').ok_or_else(|| {
format!("line {line_number}: malformed token '{token}', expected key=value")
})?;
if key.is_empty() {
return Err(format!("line {line_number}: empty key in token '{token}'"));
}
if value.is_empty() {
return Err(format!("line {line_number}: empty value for key '{key}'"));
}
if len >= KVMAP_CAPACITY {
return Err(format!(
"line {line_number}: too many key=value pairs (limit {KVMAP_CAPACITY})"
));
}
buf[len] = (key, value);
len += 1;
}
Ok(Self { buf, len })
}
fn get(&self, key: &str) -> Option<&'a str> {
self.buf[..self.len]
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| *v)
}
}
pub const MAX_SCENE_FILE_SIZE: u64 = 8 * 1024 * 1024;
pub const MAX_SCENE_SPHERES: usize = 100_000;
pub const MAX_SCENE_TRIANGLES: usize = 1_000_000;
pub const MAX_SCENE_AREA_LIGHTS: usize = 1_024;
pub const MAX_MATERIAL_NAME_LEN: usize = 128;
fn parse_f64_val(s: &str, line_number: usize) -> Result<f64, String> {
let trimmed = s.trim();
let value = match parse_f64_fast(trimmed) {
Some(v) => v,
None => f64::from_str(trimmed)
.map_err(|_| format!("line {line_number}: invalid f64 '{trimmed}'"))?,
};
if !value.is_finite() {
return Err(format!("line {line_number}: non-finite f64 '{trimmed}'"));
}
Ok(value)
}
const POW10: [f64; 23] = [
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
];
fn parse_f64_fast(token: &str) -> Option<f64> {
let bytes = token.as_bytes();
if bytes.is_empty() {
return None;
}
let mut i = 0usize;
let neg = match bytes[0] {
b'-' => {
i = 1;
true
}
b'+' => {
i = 1;
false
}
_ => false,
};
if i >= bytes.len() {
return None;
}
let mut mantissa: u64 = 0;
let mut digits: u32 = 0;
let mut frac_digits: i32 = 0;
let mut saw_digit = false;
let mut saw_dot = false;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_digit() {
saw_digit = true;
if digits >= 19 {
return None;
}
mantissa = mantissa * 10 + (b - b'0') as u64;
digits += 1;
if saw_dot {
frac_digits += 1;
}
i += 1;
} else if b == b'.' {
if saw_dot {
return None;
}
saw_dot = true;
i += 1;
} else {
break;
}
}
if !saw_digit {
return None;
}
let mut exp: i32 = -frac_digits;
if i < bytes.len() {
let b = bytes[i];
if b == b'e' || b == b'E' {
i += 1;
if i >= bytes.len() {
return None;
}
let exp_neg = match bytes[i] {
b'-' => {
i += 1;
true
}
b'+' => {
i += 1;
false
}
_ => false,
};
if i >= bytes.len() {
return None;
}
let mut e: i32 = 0;
while i < bytes.len() {
let c = bytes[i];
if !c.is_ascii_digit() {
return None;
}
e = e.checked_mul(10)?.checked_add((c - b'0') as i32)?;
if e > 308 {
return None;
}
i += 1;
}
exp = exp.checked_add(if exp_neg { -e } else { e })?;
} else {
return None;
}
}
if mantissa > (1u64 << 53) {
return None;
}
if exp.unsigned_abs() > 22 {
return None;
}
let m = mantissa as f64;
let value = if exp >= 0 {
m * POW10[exp as usize]
} else {
m / POW10[(-exp) as usize]
};
Some(if neg { -value } else { value })
}
fn parse_unit_scalar(s: &str, line_number: usize) -> Result<f64, String> {
let value = parse_f64_val(s, line_number)?;
if !(0.0..=1.0).contains(&value) {
return Err(format!(
"line {line_number}: scalar must be within [0, 1], got {value}"
));
}
Ok(value)
}
fn parse_f64_vec3(s: &str, line_number: usize) -> Result<[f64; 3], String> {
let parts: Vec<&str> = s.splitn(3, ',').collect();
if parts.len() != 3 {
return Err(format!(
"line {line_number}: expected 3 components in '{s}'"
));
}
Ok([
parse_f64_val(parts[0], line_number)?,
parse_f64_val(parts[1], line_number)?,
parse_f64_val(parts[2], line_number)?,
])
}
fn parse_unit_vec3(s: &str, line_number: usize) -> Result<[f64; 3], String> {
let raw = parse_f64_vec3(s, line_number)?;
for component in raw {
if !(0.0..=1.0).contains(&component) {
return Err(format!(
"line {line_number}: color components must be within [0, 1], got {component}"
));
}
}
Ok(raw)
}
fn parse_f64_vec2(s: &str, line_number: usize) -> Result<[f64; 2], String> {
let parts: Vec<&str> = s.splitn(2, ',').collect();
if parts.len() != 2 {
return Err(format!(
"line {line_number}: expected 2 components in '{s}'"
));
}
Ok([
parse_f64_val(parts[0], line_number)?,
parse_f64_val(parts[1], line_number)?,
])
}
fn validate_material_name(name: &str, line_number: usize) -> Result<(), String> {
if name.is_empty() {
return Err(format!("line {line_number}: empty material name"));
}
if name.len() > MAX_MATERIAL_NAME_LEN {
return Err(format!(
"line {line_number}: material name length {} exceeds limit {MAX_MATERIAL_NAME_LEN}",
name.len()
));
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(format!(
"line {line_number}: material name '{name}' contains invalid characters"
));
}
Ok(())
}
fn fmt3_into(out: &mut String, v: [f64; 3]) {
let _ = write!(out, "{},{},{}", v[0], v[1], v[2]);
}
fn fmt2_into(out: &mut String, v: [f64; 2]) {
let _ = write!(out, "{},{}", v[0], v[1]);
}