use super::*;
use indicatrix::{
geometry::cuts::StandardGemCuts,
optics::{materials::GemMaterial, raytracer::LightingPreset},
};
#[test]
fn run_export_produces_a_valid_png_for_a_tiny_scene() {
let scene = SceneSnapshot {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
light_yaw: 0.85,
light_pitch: 0.95,
material: GemMaterial::diamond(),
lighting_preset: LightingPreset::RingLights,
max_bounces: 4,
exposure: 1.0,
backdrop: 0.0,
active_planes: StandardGemCuts::standard_round_brilliant(),
facet_finishes: Vec::new(),
env_map: None,
};
let params = ExportParams {
width: 8,
height: 8,
samples_per_pixel: 1,
max_bounces: 4,
};
let cancel = AtomicBool::new(false);
let dir =
std::env::temp_dir().join(format!("indicatrix-cut-export-test-{}", std::process::id()));
let output_path = dir.join("tiny.png");
let mut progress_calls = 0;
let outcome = run_export(
&scene,
params,
ColorSpace::Srgb,
&output_path,
ComputeTarget::LocalOnly,
&[],
LocalComputeTarget::CpuGpu,
&cancel,
|_frac| {
progress_calls += 1;
},
);
match outcome {
ExportOutcome::Completed(path) => {
assert_eq!(path, output_path);
let img = image::open(&path).expect("exported file must be a valid, readable image");
assert_eq!(img.width(), 8);
assert_eq!(img.height(), 8);
}
other => panic!("expected a completed export, got {other:?}"),
}
assert!(
progress_calls > 0,
"progress must be reported at least once"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_export_with_an_hdr_environment_map_still_produces_a_valid_png() {
let scene = SceneSnapshot {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
light_yaw: 0.85,
light_pitch: 0.95,
material: GemMaterial::diamond(),
lighting_preset: LightingPreset::RingLights,
max_bounces: 4,
exposure: 1.0,
backdrop: 0.0,
active_planes: StandardGemCuts::standard_round_brilliant(),
facet_finishes: Vec::new(),
env_map: Some(std::sync::Arc::new(
indicatrix::renderer::env_map::EnvironmentMap::uniform(4, 4, [0.5, 0.5, 0.5]),
)),
};
let params = ExportParams {
width: 8,
height: 8,
samples_per_pixel: 1,
max_bounces: 4,
};
let cancel = AtomicBool::new(false);
let dir = std::env::temp_dir().join(format!(
"indicatrix-cut-export-hdr-test-{}",
std::process::id()
));
let output_path = dir.join("hdr.png");
let outcome = run_export(
&scene,
params,
ColorSpace::Srgb,
&output_path,
ComputeTarget::LocalOnly,
&[],
LocalComputeTarget::CpuGpu,
&cancel,
|_frac| {},
);
match outcome {
ExportOutcome::Completed(path) => {
let img = image::open(&path).expect("exported file must be a valid, readable image");
assert_eq!(img.width(), 8);
assert_eq!(img.height(), 8);
}
other => panic!("expected a completed export, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_export_honors_pre_set_cancellation_and_writes_no_file() {
let scene = SceneSnapshot {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
light_yaw: 0.85,
light_pitch: 0.95,
material: GemMaterial::diamond(),
lighting_preset: LightingPreset::RingLights,
max_bounces: 4,
exposure: 1.0,
backdrop: 0.0,
active_planes: StandardGemCuts::standard_round_brilliant(),
facet_finishes: Vec::new(),
env_map: None,
};
let params = ExportParams {
width: 8,
height: 8,
samples_per_pixel: 64,
max_bounces: 4,
};
let cancel = AtomicBool::new(true);
let dir = std::env::temp_dir().join(format!(
"indicatrix-cut-export-cancel-test-{}",
std::process::id()
));
let output_path = dir.join("never.png");
let outcome = run_export(
&scene,
params,
ColorSpace::Srgb,
&output_path,
ComputeTarget::LocalOnly,
&[],
LocalComputeTarget::CpuGpu,
&cancel,
|_frac| {},
);
assert!(matches!(outcome, ExportOutcome::Cancelled));
assert!(!output_path.exists());
let _ = std::fs::remove_dir_all(&dir);
}
fn tiny_scene() -> SceneSnapshot {
SceneSnapshot {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
light_yaw: 0.85,
light_pitch: 0.95,
material: GemMaterial::diamond(),
lighting_preset: LightingPreset::RingLights,
max_bounces: 4,
exposure: 1.0,
backdrop: 0.0,
active_planes: StandardGemCuts::standard_round_brilliant(),
facet_finishes: Vec::new(),
env_map: None,
}
}
#[test]
fn srgb_export_is_byte_identical_regardless_of_which_save_png_path_runs() {
let scene = tiny_scene();
let params = ExportParams {
width: 6,
height: 5,
samples_per_pixel: 2,
max_bounces: 4,
};
let dir = std::env::temp_dir().join(format!(
"indicatrix-cut-srgb-identity-test-{}",
std::process::id()
));
let path_a = dir.join("a.png");
let path_b = dir.join("b.png");
let cancel = AtomicBool::new(false);
let outcome_a = run_export(
&scene,
params,
ColorSpace::Srgb,
&path_a,
ComputeTarget::LocalOnly,
&[],
LocalComputeTarget::CpuGpu,
&cancel,
|_| {},
);
let outcome_b = run_export(
&scene,
params,
ColorSpace::Srgb,
&path_b,
ComputeTarget::LocalOnly,
&[],
LocalComputeTarget::CpuGpu,
&cancel,
|_| {},
);
assert!(matches!(outcome_a, ExportOutcome::Completed(_)));
assert!(matches!(outcome_b, ExportOutcome::Completed(_)));
let bytes_a = std::fs::read(&path_a).unwrap();
let bytes_b = std::fs::read(&path_b).unwrap();
assert_eq!(
bytes_a, bytes_b,
"two Srgb exports of the identical scene must match exactly"
);
assert!(
!contains_bytes(&bytes_a, b"iCCP"),
"an Srgb export must never carry an ICC profile chunk"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn wide_gamut_export_embeds_an_icc_profile_chunk() {
for color_space in [ColorSpace::DisplayP3, ColorSpace::Rec2020] {
let scene = tiny_scene();
let params = ExportParams {
width: 6,
height: 5,
samples_per_pixel: 2,
max_bounces: 4,
};
let cancel = AtomicBool::new(false);
let dir = std::env::temp_dir().join(format!(
"indicatrix-cut-wide-gamut-test-{color_space:?}-{}",
std::process::id()
));
let output_path = dir.join("wide.png");
let outcome = run_export(
&scene,
params,
color_space,
&output_path,
ComputeTarget::LocalOnly,
&[],
LocalComputeTarget::CpuGpu,
&cancel,
|_| {},
);
match outcome {
ExportOutcome::Completed(path) => {
let bytes = std::fs::read(&path).unwrap();
assert!(
contains_bytes(&bytes, b"iCCP"),
"{color_space:?}: expected an iCCP chunk in the exported PNG"
);
let img = image::open(&path)
.unwrap_or_else(|e| panic!("{color_space:?}: not a readable PNG: {e}"));
assert_eq!(img.width(), 6);
assert_eq!(img.height(), 5);
}
other => panic!("{color_space:?}: expected a completed export, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}