use super::mesh_cache::CachedMesh;
use glam::{DVec3, Vec3};
use indicatrix::{geometry::stone_metrics::SolidMesh, optics::raytracer::Camera};
const NEAR_EPS: f32 = 1e-4;
const HATCH_PERIOD: i32 = 6;
const EDGE_DEPTH_BIAS: f32 = 5e-2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FillMode {
#[default]
Opaque,
Transparent,
}
#[derive(Debug, Clone)]
pub struct SolidStyle {
pub base_color: [u8; 3],
pub ambient: f32,
pub diffuse: f32,
pub rim_strength: f32,
pub key_light_dir: Vec3,
pub edge_color: [u8; 3],
pub pending_color: [u8; 3],
pub hatch_color: [u8; 3],
pub selected_color: [u8; 3],
pub background: [u8; 4],
pub fill_mode: FillMode,
pub flagged: Vec<bool>,
pub pending: Vec<bool>,
pub selected: Vec<bool>,
}
impl Default for SolidStyle {
fn default() -> Self {
Self {
base_color: [200, 205, 215],
ambient: 0.25,
diffuse: 0.75,
rim_strength: 0.18,
key_light_dir: Vec3::new(0.4, 0.8, 0.5).normalize(),
edge_color: [30, 32, 38],
pending_color: [235, 170, 40],
hatch_color: [90, 40, 40],
selected_color: [70, 160, 235],
background: [0, 0, 0, 0],
fill_mode: FillMode::Opaque,
flagged: Vec::new(),
pending: Vec::new(),
selected: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct SolidRasterizer {
pub width: u32,
pub height: u32,
pub depth: Vec<f32>,
pub color: Vec<u8>,
pub pick: Vec<u32>,
ring_scratch: Vec<DVec3>,
dedup_scratch: Vec<DVec3>,
edge_points: Vec<(f32, f32, f32)>,
edge_ranges: Vec<(usize, usize, usize)>,
}
impl SolidRasterizer {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
let n = (width as usize) * (height as usize);
Self {
width,
height,
depth: vec![f32::INFINITY; n],
color: vec![0u8; n * 4],
pick: vec![0u32; n],
ring_scratch: Vec::new(),
dedup_scratch: Vec::new(),
edge_points: Vec::new(),
edge_ranges: Vec::new(),
}
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == self.width && height == self.height {
return;
}
*self = Self::new(width, height);
}
#[must_use]
pub fn pick_at(&self, x: u32, y: u32) -> Option<u32> {
if x >= self.width || y >= self.height {
return None;
}
let v = self.pick[(y * self.width + x) as usize];
(v != 0).then(|| v - 1)
}
pub fn render(&mut self, mesh: &SolidMesh, camera: &Camera, style: &SolidStyle) {
self.clear(style.background);
self.edge_points.clear();
self.edge_ranges.clear();
if self.width == 0 || self.height == 0 {
return;
}
let (w, h) = (self.width as f32, self.height as f32);
let facet_count = mesh.rings.iter().map(|(id, _)| id + 1).max().unwrap_or(0);
let mut facet_normal: Vec<Option<DVec3>> = vec![None; facet_count];
for (&id, &normal) in mesh.facet_id.iter().zip(&mesh.normals) {
if let Some(slot) = facet_normal.get_mut(id)
&& slot.is_none()
{
*slot = Some(normal);
}
}
let mut ring_scratch = std::mem::take(&mut self.ring_scratch);
let mut dedup_scratch = std::mem::take(&mut self.dedup_scratch);
let mut screen: Vec<(f32, f32, f32)> = Vec::new();
for (facet_id, ring) in &mesh.rings {
let Some(Some(normal)) = facet_normal.get(*facet_id).copied() else {
continue;
};
let Some(&first) = ring.first() else {
continue;
};
let normal = to_vec3(normal);
let to_camera = camera.origin - to_vec3(first);
if normal.dot(to_camera) <= 0.0 {
continue;
}
simplify_ring(ring, &mut dedup_scratch, &mut ring_scratch);
if ring_scratch.len() < 3 {
continue;
}
screen.clear();
let mut all_in_front = true;
for &p in &ring_scratch {
if let Some(sp) = project(camera, p, w, h) {
screen.push(sp);
} else {
all_in_front = false;
break;
}
}
if !all_in_front {
continue;
}
let color = shade(normal, to_vec3(first), camera, style);
self.fill_convex_polygon(&screen, *facet_id, color, style);
self.edge_ranges
.push((*facet_id, self.edge_points.len(), screen.len()));
self.edge_points.extend_from_slice(&screen);
}
self.ring_scratch = ring_scratch;
self.dedup_scratch = dedup_scratch;
let edge_ranges = std::mem::take(&mut self.edge_ranges);
let edge_points = std::mem::take(&mut self.edge_points);
for &(facet_id, start, count) in &edge_ranges {
let edge_color = if style.pending.get(facet_id).copied().unwrap_or(false) {
style.pending_color
} else if style.selected.get(facet_id).copied().unwrap_or(false) {
style.selected_color
} else {
style.edge_color
};
let pts = &edge_points[start..start + count];
for (i, &a) in pts.iter().enumerate() {
let b = pts[(i + 1) % count];
self.draw_edge(a, b, edge_color);
}
}
self.edge_ranges = edge_ranges;
self.edge_points = edge_points;
}
pub fn render_prepared(&mut self, prepared: &CachedMesh, camera: &Camera, style: &SolidStyle) {
self.clear(style.background);
self.edge_points.clear();
self.edge_ranges.clear();
if self.width == 0 || self.height == 0 {
return;
}
let (w, h) = (self.width as f32, self.height as f32);
let mesh = &prepared.mesh;
let facet_count = mesh.rings.iter().map(|(id, _)| id + 1).max().unwrap_or(0);
let mut facet_normal: Vec<Option<DVec3>> = vec![None; facet_count];
for (&id, &normal) in mesh.facet_id.iter().zip(&mesh.normals) {
if let Some(slot) = facet_normal.get_mut(id)
&& slot.is_none()
{
*slot = Some(normal);
}
}
let mut screen: Vec<(f32, f32, f32)> = Vec::new();
for (facet_id, ring) in &prepared.rings {
let Some(Some(normal)) = facet_normal.get(*facet_id).copied() else {
continue;
};
let Some(&first) = ring.first() else {
continue;
};
let normal = to_vec3(normal);
let to_camera = camera.origin - to_vec3(first);
if normal.dot(to_camera) <= 0.0 {
continue;
}
screen.clear();
let mut all_in_front = true;
for &p in ring {
if let Some(sp) = project(camera, p, w, h) {
screen.push(sp);
} else {
all_in_front = false;
break;
}
}
if !all_in_front {
continue;
}
let color = shade(normal, to_vec3(first), camera, style);
self.fill_convex_polygon(&screen, *facet_id, color, style);
self.edge_ranges
.push((*facet_id, self.edge_points.len(), screen.len()));
self.edge_points.extend_from_slice(&screen);
}
let edge_ranges = std::mem::take(&mut self.edge_ranges);
let edge_points = std::mem::take(&mut self.edge_points);
for &(facet_id, start, count) in &edge_ranges {
let edge_color = if style.pending.get(facet_id).copied().unwrap_or(false) {
style.pending_color
} else if style.selected.get(facet_id).copied().unwrap_or(false) {
style.selected_color
} else {
style.edge_color
};
let pts = &edge_points[start..start + count];
for (i, &a) in pts.iter().enumerate() {
let b = pts[(i + 1) % count];
self.draw_edge(a, b, edge_color);
}
}
self.edge_ranges = edge_ranges;
self.edge_points = edge_points;
}
fn clear(&mut self, background: [u8; 4]) {
self.depth.fill(f32::INFINITY);
self.pick.fill(0);
let (pixels, _remainder) = self.color.as_chunks_mut::<4>();
for pixel in pixels {
pixel.copy_from_slice(&background);
}
}
fn fill_convex_polygon(
&mut self,
verts: &[(f32, f32, f32)],
facet_id: usize,
color: [u8; 3],
style: &SolidStyle,
) {
let n = verts.len();
if n < 3 {
return;
}
let (x0, y0, a0) = verts[0];
let mut best = (1usize, 2usize);
let mut best_area = 0.0f32;
for i in 1..n - 1 {
let (xi, yi, _) = verts[i];
let (xj, yj, _) = verts[i + 1];
let area = edge_fn(x0, y0, xi, yi, xj, yj);
if area.abs() > best_area.abs() {
best_area = area;
best = (i, i + 1);
}
}
if best_area.abs() < 1e-8 {
return; }
let (x1, y1, a1) = verts[best.0];
let (x2, y2, a2) = verts[best.1];
let inv_area = 1.0 / best_area;
let (inv_a0, inv_a1, inv_a2) = (1.0 / a0, 1.0 / a1, 1.0 / a2);
let grad_x =
(y1 - y2).mul_add(inv_a0, (y2 - y0).mul_add(inv_a1, (y0 - y1) * inv_a2)) * inv_area;
let grad_y =
(x2 - x1).mul_add(inv_a0, (x0 - x2).mul_add(inv_a1, (x1 - x0) * inv_a2)) * inv_area;
let plane_c = inv_a0 - grad_x.mul_add(x0, grad_y * y0);
let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY);
for &(_, y, _) in verts {
min_y = min_y.min(y);
max_y = max_y.max(y);
}
let row_start = min_y.floor().max(0.0) as i32;
let row_end = max_y.ceil().min((self.height - 1) as f32) as i32;
if row_start > row_end {
return; }
let flagged = style.flagged.get(facet_id).copied().unwrap_or(false);
let selected = style.selected.get(facet_id).copied().unwrap_or(false);
let selected_fill = selected.then(|| blend_toward(color, style.selected_color, 0.35));
let alpha: u8 = if style.fill_mode == FillMode::Transparent {
0
} else {
255
};
let max_px = (self.width - 1) as f32;
for py in row_start..=row_end {
let sy = py as f32 + 0.5;
let (mut x_left, mut x_right) = (f32::INFINITY, f32::NEG_INFINITY);
let mut crossings = 0u32;
for (i, &(px_a, py_a, _)) in verts.iter().enumerate() {
let (px_b, py_b, _) = verts[(i + 1) % n];
if (py_a <= sy) == (py_b <= sy) {
continue; }
let t = (sy - py_a) / (py_b - py_a);
let x = t.mul_add(px_b - px_a, px_a);
x_left = x_left.min(x);
x_right = x_right.max(x);
crossings += 1;
}
if crossings < 2 {
continue;
}
let col_start = (x_left - 0.5).ceil().max(0.0) as i32;
let col_end = (x_right - 0.5).floor().min(max_px) as i32;
if col_start > col_end {
continue;
}
let row_base = (py as u32 * self.width) as usize;
for px in col_start..=col_end {
let sx = px as f32 + 0.5;
let inv_a = grad_x.mul_add(sx, grad_y.mul_add(sy, plane_c));
if inv_a <= 0.0 {
continue; }
let depth = 1.0 / inv_a;
let idx = row_base + px as usize;
if depth < self.depth[idx] {
self.depth[idx] = depth;
self.pick[idx] = facet_id as u32 + 1;
let stripe = (px + py).rem_euclid(HATCH_PERIOD * 2) < HATCH_PERIOD;
let painted = if flagged && stripe {
style.hatch_color
} else {
selected_fill.unwrap_or(color)
};
let o = idx * 4;
self.color[o] = painted[0];
self.color[o + 1] = painted[1];
self.color[o + 2] = painted[2];
self.color[o + 3] = alpha;
}
}
}
}
fn draw_edge(&mut self, from: (f32, f32, f32), to: (f32, f32, f32), color: [u8; 3]) {
let (x0, y0, a0) = (from.0.round() as i32, from.1.round() as i32, from.2);
let (x1, y1, a1) = (to.0.round() as i32, to.1.round() as i32, to.2);
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx: i32 = if x0 < x1 { 1 } else { -1 };
let sy: i32 = if y0 < y1 { 1 } else { -1 };
let steps_total = dx.max(-dy).max(1);
let (inv_a0, inv_a1) = (1.0 / a0, 1.0 / a1);
let (mut cx, mut cy) = (x0, y0);
let mut err = dx + dy;
let mut step = 0;
loop {
if cx >= 0 && cy >= 0 && (cx as u32) < self.width && (cy as u32) < self.height {
let frac = step as f32 / steps_total as f32;
let inv_a = (1.0 - frac).mul_add(inv_a0, frac * inv_a1);
if inv_a > 0.0 {
let depth = 1.0 / inv_a;
let idx = (cy as u32 * self.width + cx as u32) as usize;
if depth <= self.depth[idx] + EDGE_DEPTH_BIAS {
let offset = idx * 4;
self.color[offset] = color[0];
self.color[offset + 1] = color[1];
self.color[offset + 2] = color[2];
self.color[offset + 3] = 255;
}
}
}
if cx == x1 && cy == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
cx += sx;
}
if e2 <= dx {
err += dx;
cy += sy;
}
step += 1;
}
}
}
pub(super) fn simplify_ring(ring: &[DVec3], dedup: &mut Vec<DVec3>, out: &mut Vec<DVec3>) {
const SIN_TOL: f64 = 1e-6;
dedup.clear();
out.clear();
let Some(&first) = ring.first() else {
return;
};
let (mut lo, mut hi) = (first, first);
for &p in ring {
lo = lo.min(p);
hi = hi.max(p);
}
let merge_eps = (hi - lo).length().max(1e-12) * 1e-7;
for &p in ring {
if dedup
.last()
.is_some_and(|&last| (p - last).length() <= merge_eps)
{
continue;
}
dedup.push(p);
}
while dedup.len() > 1 && (dedup[0] - dedup[dedup.len() - 1]).length() <= merge_eps {
dedup.pop();
}
if dedup.len() < 3 {
out.extend_from_slice(dedup);
return;
}
let n = dedup.len();
let mut prev = dedup[n - 1];
for i in 0..n {
let cur = dedup[i];
let next = dedup[(i + 1) % n];
let e1 = cur - prev;
let e2 = next - cur;
let collinear = e1.cross(e2).length() <= SIN_TOL * e1.length() * e2.length();
if !collinear {
out.push(cur);
prev = cur;
}
}
}
const fn to_vec3(v: DVec3) -> Vec3 {
Vec3::new(v.x as f32, v.y as f32, v.z as f32)
}
fn project(camera: &Camera, point: DVec3, width: f32, height: f32) -> Option<(f32, f32, f32)> {
let delta = to_vec3(point) - camera.origin;
let depth = delta.dot(camera.forward);
if depth <= NEAR_EPS {
return None;
}
let right_comp = delta.dot(camera.right);
let up_comp = delta.dot(camera.up);
let u_ratio = right_comp / depth;
let v_ratio = up_comp / depth;
let aspect = width / height;
let screen_x = width * (0.5 + u_ratio / (2.0 * aspect * camera.fov_tan));
let screen_y = height * (0.5 - v_ratio / (2.0 * camera.fov_tan));
Some((screen_x, screen_y, depth))
}
fn edge_fn(ax: f32, ay: f32, bx: f32, by: f32, px: f32, py: f32) -> f32 {
(by - ay).mul_add(-(px - ax), (bx - ax) * (py - ay))
}
fn blend_toward(base: [u8; 3], target: [u8; 3], amount: f32) -> [u8; 3] {
std::array::from_fn(|i| {
let b = f32::from(base[i]);
let t = f32::from(target[i]);
amount.mul_add(t - b, b).round().clamp(0.0, 255.0) as u8
})
}
fn shade(normal: Vec3, world_point: Vec3, camera: &Camera, style: &SolidStyle) -> [u8; 3] {
let n = normal.normalize();
let light_dir = style.key_light_dir.normalize();
let view_dir = (camera.origin - world_point).normalize();
let n_dot_l = n.dot(light_dir).max(0.0);
let n_dot_v = n.dot(view_dir).max(0.0);
let rim = (1.0 - n_dot_v).powi(2) * style.rim_strength;
let intensity = style
.diffuse
.mul_add(n_dot_l, style.ambient + rim)
.clamp(0.0, 1.0);
[
(f32::from(style.base_color[0]) * intensity)
.round()
.clamp(0.0, 255.0) as u8,
(f32::from(style.base_color[1]) * intensity)
.round()
.clamp(0.0, 255.0) as u8,
(f32::from(style.base_color[2]) * intensity)
.round()
.clamp(0.0, 255.0) as u8,
]
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::{
cuts::StandardGemCuts,
meet_solver,
plane::GpuFacetPlane,
stone_metrics::{SolidStatus, build_solid_mesh},
};
fn unit_box_mesh() -> SolidMesh {
let planes = vec![
(DVec3::X, 1.0),
(DVec3::NEG_X, 1.0),
(DVec3::Y, 0.6),
(DVec3::NEG_Y, 0.6),
(DVec3::Z, 1.0),
(DVec3::NEG_Z, 1.0),
];
match build_solid_mesh(&planes) {
SolidStatus::Closed(mesh) => mesh,
other => panic!("box fixture must close: {other:?}"),
}
}
#[test]
fn unit_cube_renders_expected_coverage_and_depth_ordering() {
let mesh = unit_box_mesh();
let camera = Camera::new(0.0, 0.0, 5.0, 42.0);
let style = SolidStyle::default();
let mut rasterizer = SolidRasterizer::new(64, 64);
rasterizer.render(&mesh, &camera, &style);
assert_eq!(
rasterizer.pick_at(32, 32),
Some(4),
"screen center must show the +Z facet"
);
assert_eq!(
rasterizer.pick_at(0, 0),
None,
"image corner must be outside the box's silhouette"
);
let mut painted = 0;
for &p in &rasterizer.pick {
if p != 0 {
painted += 1;
assert_eq!(p, 5, "every painted pixel must be the +Z facet (id 4)");
}
}
assert!(
painted > 100,
"expected a real silhouette, got {painted} px"
);
}
fn two_quads_mesh() -> SolidMesh {
let mut mesh = SolidMesh::default();
for (facet_id, z, half_extent) in [(0usize, 0.0f64, 0.5f64), (1usize, -2.0f64, 1.0f64)] {
let normal = DVec3::new(0.0, 0.0, 1.0);
let corners = [
DVec3::new(-half_extent, -half_extent, z),
DVec3::new(half_extent, -half_extent, z),
DVec3::new(half_extent, half_extent, z),
DVec3::new(-half_extent, half_extent, z),
];
let base = mesh.positions.len() as u32;
for c in corners {
mesh.positions.push(c);
mesh.normals.push(normal);
mesh.facet_id.push(facet_id);
}
mesh.indices
.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
mesh.rings.push((facet_id, corners.to_vec()));
}
mesh
}
fn two_quads_camera() -> Camera {
Camera::new(0.0, 0.0, 5.0, 42.0)
}
#[test]
fn a_facet_hidden_behind_another_never_paints() {
let mesh = two_quads_mesh();
let camera = two_quads_camera();
let style = SolidStyle::default();
let mut rasterizer = SolidRasterizer::new(64, 64);
rasterizer.render(&mesh, &camera, &style);
assert_eq!(rasterizer.pick_at(32, 32), Some(0));
}
#[test]
fn pick_buffer_returns_the_right_facet_id_at_known_pixels() {
let mesh = two_quads_mesh();
let camera = two_quads_camera();
let style = SolidStyle::default();
let mut rasterizer = SolidRasterizer::new(64, 64);
rasterizer.render(&mesh, &camera, &style);
assert_eq!(
rasterizer.pick_at(32, 32),
Some(0),
"center: covered by both, near facet (0) wins"
);
assert_eq!(
rasterizer.pick_at(42, 32),
Some(1),
"just outside the near quad's footprint, inside the far quad's"
);
assert_eq!(
rasterizer.pick_at(60, 32),
None,
"outside both quads' footprints"
);
}
#[test]
fn flagged_facets_are_visibly_hatched() {
let mesh = unit_box_mesh();
let mut flagged = vec![false; 6];
flagged[4] = true; let hatched_style = SolidStyle {
flagged,
..SolidStyle::default()
};
let mut plain = SolidRasterizer::new(64, 64);
plain.render(
&mesh,
&Camera::new(0.0, 0.0, 5.0, 42.0),
&SolidStyle::default(),
);
let mut hatched = SolidRasterizer::new(64, 64);
hatched.render(&mesh, &Camera::new(0.0, 0.0, 5.0, 42.0), &hatched_style);
assert_ne!(
plain.color, hatched.color,
"hatching a visible facet must change at least one pixel"
);
}
#[test]
fn selected_facets_get_a_visible_tint_and_the_selected_edge_color() {
let mesh = unit_box_mesh();
let mut selected = vec![false; 6];
selected[4] = true; let style = SolidStyle {
selected,
selected_color: [0, 0, 255],
edge_color: [10, 10, 10],
..SolidStyle::default()
};
let mut plain = SolidRasterizer::new(64, 64);
plain.render(
&mesh,
&Camera::new(0.0, 0.0, 5.0, 42.0),
&SolidStyle::default(),
);
let mut tinted = SolidRasterizer::new(64, 64);
tinted.render(&mesh, &Camera::new(0.0, 0.0, 5.0, 42.0), &style);
assert_ne!(
plain.color, tinted.color,
"tinting a selected facet must change at least one pixel"
);
let found_selected_edge = tinted
.color
.as_chunks::<4>()
.0
.iter()
.any(|px| px[0] == 0 && px[1] == 0 && px[2] == 255);
assert!(
found_selected_edge,
"expected the selected edge color somewhere on screen"
);
}
#[test]
fn pending_facets_get_the_pending_edge_color() {
let mesh = unit_box_mesh();
let mut pending = vec![false; 6];
pending[4] = true;
let style = SolidStyle {
pending,
pending_color: [255, 0, 0],
edge_color: [10, 10, 10],
..SolidStyle::default()
};
let mut rasterizer = SolidRasterizer::new(64, 64);
rasterizer.render(&mesh, &Camera::new(0.0, 0.0, 5.0, 42.0), &style);
let found_pending_edge = rasterizer
.color
.as_chunks::<4>()
.0
.iter()
.any(|px| px[0] == 255 && px[1] == 0 && px[2] == 0);
assert!(
found_pending_edge,
"expected the pending edge color somewhere on screen"
);
}
fn rbc_planes() -> Vec<(DVec3, f64)> {
StandardGemCuts::standard_round_brilliant()
.into_iter()
.map(GpuFacetPlane::to_halfspace_f64)
.collect()
}
fn crackotto_step_planes() -> Vec<(DVec3, f64)> {
const TEXT: &str = include_str!(
"../../../../../crates/indicatrix-cut-core/src/optimize_cost_probe_crackotto_step.asc"
);
let schedule = indicatrix_formats::asc::parse_asc(TEXT).expect("fixture must parse");
let mut inputs = meet_solver::meet_tier_inputs_from_asc(&schedule);
let blocks = meet_solver::classify_blocks(&inputs);
for block in [
meet_solver::Block::Crown,
meet_solver::Block::Pavilion,
meet_solver::Block::Girdle,
] {
let anchored = inputs.iter().zip(&blocks).any(|(t, &b)| {
b == block && matches!(t.constraint, meet_solver::MeetConstraint::ScaleReference(_))
});
if anchored {
continue;
}
if let Some(i) = (0..inputs.len()).find(|&i| blocks[i] == block) {
inputs[i].constraint =
meet_solver::MeetConstraint::ScaleReference(schedule.tiers[i].mast);
}
}
let normals = meet_solver::tier_instance_normals(schedule.gear_teeth_abs(), &inputs);
let solved = meet_solver::solve_meet_points(schedule.gear_teeth_abs(), &inputs);
normals
.iter()
.zip(solved.iter().map(|s| s.mast))
.flat_map(|(ns, m)| ns.iter().map(move |&n| (n, m)))
.collect()
}
#[test]
#[ignore = "writes a PNG to the system temp dir -- a visual smoke test, not a \
correctness check; run explicitly with --ignored"]
fn renders_the_standard_round_brilliant_without_panicking() {
let planes = rbc_planes();
let mesh = match build_solid_mesh(&planes) {
SolidStatus::Closed(mesh) => mesh,
other => panic!("RBC-445 must close: {other:?}"),
};
let camera = Camera::new(0.6, 0.35, 3.0, 42.0);
let style = SolidStyle::default();
let mut rasterizer = SolidRasterizer::new(800, 600);
rasterizer.render(&mesh, &camera, &style);
let visible = rasterizer.pick.iter().filter(|&&p| p != 0).count();
assert!(
visible > 1000,
"expected a substantial visible silhouette, got {visible} px"
);
let image = image::RgbaImage::from_raw(800, 600, rasterizer.color.clone())
.expect("color buffer length must match 800x600 RGBA8");
let path = std::env::temp_dir().join("indicatrix_cut_solid_preview_rbc.png");
image
.save(&path)
.expect("PNG write to the temp dir must succeed");
println!("wrote {}", path.display());
}
#[test]
#[ignore = "timing measurement, not a correctness check -- run with \
--release --ignored --nocapture"]
fn timing_rbc_445_800x600() {
let planes = rbc_planes();
let mesh = match build_solid_mesh(&planes) {
SolidStatus::Closed(mesh) => mesh,
other => panic!("RBC-445 must close: {other:?}"),
};
let camera = Camera::new(0.6, 0.35, 3.0, 42.0);
let style = SolidStyle::default();
let mut rasterizer = SolidRasterizer::new(800, 600);
rasterizer.render(&mesh, &camera, &style);
let iters = 200u32;
let start = std::time::Instant::now();
for _ in 0..iters {
rasterizer.render(&mesh, &camera, &style);
}
let per_frame = start.elapsed() / iters;
println!(
"RBC-445 800x600: {per_frame:?} per frame over {iters} iterations (target < 1 ms)"
);
}
#[test]
#[ignore = "timing measurement, not a correctness check -- run with \
--release --ignored --nocapture"]
fn timing_crackotto_step_103_tier_800x600() {
let planes = crackotto_step_planes();
let mesh = match build_solid_mesh(&planes) {
SolidStatus::Closed(mesh) => mesh,
other => panic!("CrackOtto-Step must close: {other:?}"),
};
let camera = Camera::new(0.6, 0.35, 3.0, 42.0);
let style = SolidStyle::default();
let mut rasterizer = SolidRasterizer::new(800, 600);
rasterizer.render(&mesh, &camera, &style);
let iters = 100u32;
let start = std::time::Instant::now();
for _ in 0..iters {
rasterizer.render(&mesh, &camera, &style);
}
let per_frame = start.elapsed() / iters;
println!(
"CrackOtto-Step (103 tiers) 800x600: {per_frame:?} per frame over {iters} \
iterations (target < 5 ms)"
);
}
}