use uzor::types::Rect;
use uzor_graph::camera3d::Camera3D;
use uzor_graph::engine3d::GraphEngine3D;
use uzor_graph::graph::Graph;
use uzor_graph::interaction::pick3d;
use uzor_graph::interaction::pick3d::project_world_to_screen;
use uzor_graph::layout::force_directed_3d::ForceDirectedLayout3D;
use uzor_graph::particle::Particle;
use uzor_graph::render3d;
use uzor_graph::NodeIndex;
use uzor_urx_3d::{Mesh, MeshLit, PerspectiveCamera, Renderer3D, Scene3D, Vec3};
use std::collections::HashSet;
use std::sync::Arc;
const W: u32 = 128;
const H: u32 = 128;
const COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
fn init_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("uzor-graph-render3d-test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::default(),
}))
.ok()
}
fn make_target(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("uzor-graph-render3d-target"),
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: COLOR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
(tex, view)
}
fn readback_rgba(device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture) -> Vec<u8> {
let aligned_stride = (W * 4 + 255) & !255;
let buf_size = (aligned_stride * H) as u64;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("uzor-graph-render3d-readback"),
size: buf_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo { texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(aligned_stride), rows_per_image: Some(H) },
},
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
);
queue.submit(Some(enc.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
rx.recv().expect("map_async callback fired").expect("buffer map succeeded");
let raw = slice.get_mapped_range();
let mut out = Vec::with_capacity((W * H * 4) as usize);
for row in 0..H as usize {
let row_start = row * aligned_stride as usize;
let row_end = row_start + (W * 4) as usize;
out.extend_from_slice(&raw[row_start..row_end]);
}
drop(raw);
staging.unmap();
out
}
#[inline]
fn at(buf: &[u8], x: u32, y: u32) -> [u8; 4] {
let idx = ((y * W + x) * 4) as usize;
[buf[idx], buf[idx + 1], buf[idx + 2], buf[idx + 3]]
}
fn brightness(p: [u8; 4]) -> u32 {
p[0] as u32 + p[1] as u32 + p[2] as u32
}
fn brightest_near(buf: &[u8], cx: u32, cy: u32, radius: i64) -> [u8; 4] {
let mut best = at(buf, cx, cy);
let mut best_b = brightness(best);
for dy in -radius..=radius {
for dx in -radius..=radius {
let x = (cx as i64 + dx).clamp(0, (W - 1) as i64) as u32;
let y = (cy as i64 + dy).clamp(0, (H - 1) as i64) as u32;
let p = at(buf, x, y);
let b = brightness(p);
if b > best_b {
best = p;
best_b = b;
}
}
}
best
}
type DemoGraph = Graph<(), ()>;
fn two_node_graph() -> (DemoGraph, [uzor_graph::graph::NodeIndex; 2]) {
let mut graph = DemoGraph::new();
let a = graph.push_node((), "a", "cat-a", 2.5);
let b = graph.push_node((), "b", "cat-b", 2.5);
graph.push_edge(a, b, 1.0, ());
(graph, [a, b])
}
fn head_on_engine() -> GraphEngine3D<(), (), ForceDirectedLayout3D> {
let (graph, _ids) = two_node_graph();
let mut engine = GraphEngine3D::new(graph, ForceDirectedLayout3D::default());
engine.particles[0] = Particle::at3(-6.0, 0.0, 0.0);
engine.particles[1] = Particle::at3(6.0, 0.0, 0.0);
engine.camera = Camera3D { target: Vec3::ZERO, distance: 20.0, yaw: 0.0, pitch: 0.0, ..Camera3D::default() };
engine
}
#[test]
#[ignore]
fn build_scene_renders_visually_distinct_node_and_edge_pixels() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let engine = head_on_engine();
let scene = engine.build_scene(H as f64);
assert_eq!(scene.nodes.len(), 3, "2 node spheres + 1 edge line");
let aspect = W as f32 / H as f32;
let camera = engine.camera(aspect);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
let (tex, view) = make_target(&device);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc, &view, &camera, &scene);
queue.submit(Some(enc.finish()));
let px = readback_rgba(&device, &queue, &tex);
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let (ax, ay) = project_world_to_screen(&camera, Vec3::new(-6.0, 0.0, 0.0), viewport).expect("node a is in front of the eye");
let (bx, by) = project_world_to_screen(&camera, Vec3::new(6.0, 0.0, 0.0), viewport).expect("node b is in front of the eye");
let (mx, my) = project_world_to_screen(&camera, Vec3::ZERO, viewport).expect("edge midpoint is in front of the eye");
let a_px = at(&px, ax.round() as u32, ay.round() as u32);
let b_px = at(&px, bx.round() as u32, by.round() as u32);
let mid_px = brightest_near(&px, mx.round() as u32, my.round() as u32, 2);
let corner_px = at(&px, 2, 2);
let far_corner_px = at(&px, W - 3, H - 3);
eprintln!("(ax,ay)=({ax},{ay}) (bx,by)=({bx},{by}) (mx,my)=({mx},{my})");
eprintln!("a={a_px:?} b={b_px:?} mid={mid_px:?} corner={corner_px:?} far_corner={far_corner_px:?}");
assert_eq!(
corner_px, far_corner_px,
"two corners far from every node/edge should both be pure (tonemapped) background: {corner_px:?} vs {far_corner_px:?}"
);
assert!(
brightness(a_px) > brightness(corner_px) + 30,
"node a should be visually distinct from the background: {a_px:?} vs {corner_px:?}"
);
assert!(
brightness(b_px) > brightness(corner_px) + 30,
"node b should be visually distinct from the background: {b_px:?} vs {corner_px:?}"
);
assert!(
brightness(mid_px) > brightness(corner_px) + 20,
"the edge line should be visible near its midpoint: {mid_px:?} vs {corner_px:?}"
);
assert!(
a_px != mid_px || b_px != mid_px,
"node spheres and the edge line should not be pixel-identical (both are drawn, not just one covering the other): a={a_px:?} b={b_px:?} mid={mid_px:?}"
);
}
#[test]
#[ignore]
fn camera_orbit_changes_pixels() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let mut engine = head_on_engine();
let scene = engine.build_scene(H as f64);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
let aspect = W as f32 / H as f32;
let cam_a = engine.camera(aspect);
let (tex_a, view_a) = make_target(&device);
let mut enc_a = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc_a, &view_a, &cam_a, &scene);
queue.submit(Some(enc_a.finish()));
let px_a = readback_rgba(&device, &queue, &tex_a);
engine.camera.orbit(600.0, 250.0);
let cam_b = engine.camera(aspect);
let (tex_b, view_b) = make_target(&device);
let mut enc_b = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc_b, &view_b, &cam_b, &scene);
queue.submit(Some(enc_b.finish()));
let px_b = readback_rgba(&device, &queue, &tex_b);
let total = (W * H) as usize;
let mut diff = 0usize;
for i in 0..total {
let a = &px_a[i * 4..i * 4 + 3];
let b = &px_b[i * 4..i * 4 + 3];
let d = (a[0] as i16 - b[0] as i16).unsigned_abs() as u32
+ (a[1] as i16 - b[1] as i16).unsigned_abs() as u32
+ (a[2] as i16 - b[2] as i16).unsigned_abs() as u32;
if d > 20 {
diff += 1;
}
}
let pct = (diff as f32 / total as f32) * 100.0;
eprintln!("differing pixels: {diff}/{total} ({pct:.1}%)");
assert!(pct > 5.0, "expected Camera3D::orbit to change >5% of pixels, got {pct:.1}%");
}
#[test]
#[ignore]
fn build_scene_renders_correctly_with_msaa_armed_at_sample_count_4() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let engine = head_on_engine();
let scene = engine.build_scene(H as f64);
assert_eq!(scene.nodes.len(), 3, "2 node spheres (Lit) + 1 edge line (Line) — MSAA covers Unlit+Lit+Line, all sample-count-matched pipeline pairs");
let aspect = W as f32 / H as f32;
let camera = engine.camera(aspect);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
r.set_sample_count(&device, 4);
assert_eq!(r.sample_count(), 4, "set_sample_count(4) must actually arm MSAA");
let (tex, view) = make_target(&device);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc, &view, &camera, &scene);
queue.submit(Some(enc.finish()));
let px = readback_rgba(&device, &queue, &tex);
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let (ax, ay) = project_world_to_screen(&camera, Vec3::new(-6.0, 0.0, 0.0), viewport).expect("node a is in front of the eye");
let (bx, by) = project_world_to_screen(&camera, Vec3::new(6.0, 0.0, 0.0), viewport).expect("node b is in front of the eye");
let (mx, my) = project_world_to_screen(&camera, Vec3::ZERO, viewport).expect("edge midpoint is in front of the eye");
let a_px = at(&px, ax.round() as u32, ay.round() as u32);
let b_px = at(&px, bx.round() as u32, by.round() as u32);
let mid_px = brightest_near(&px, mx.round() as u32, my.round() as u32, 2);
let corner_px = at(&px, 2, 2);
let far_corner_px = at(&px, W - 3, H - 3);
eprintln!("MSAA sample_count=4: a={a_px:?} b={b_px:?} mid={mid_px:?} corner={corner_px:?} far_corner={far_corner_px:?}");
assert_eq!(corner_px, far_corner_px, "MSAA-armed background corners must still both be pure (resolved+tonemapped) background: {corner_px:?} vs {far_corner_px:?}");
assert!(brightness(a_px) > brightness(corner_px) + 30, "node a must still be visually distinct under MSAA: {a_px:?} vs {corner_px:?}");
assert!(brightness(b_px) > brightness(corner_px) + 30, "node b must still be visually distinct under MSAA: {b_px:?} vs {corner_px:?}");
assert!(brightness(mid_px) > brightness(corner_px) + 20, "the edge line must still be visible near its midpoint under MSAA: {mid_px:?} vs {corner_px:?}");
r.set_sample_count(&device, 1);
assert_eq!(r.sample_count(), 1, "set_sample_count(1) must disarm MSAA");
let (tex2, view2) = make_target(&device);
let mut enc2 = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc2, &view2, &camera, &scene);
queue.submit(Some(enc2.finish()));
let px2 = readback_rgba(&device, &queue, &tex2);
let a_px2 = at(&px2, ax.round() as u32, ay.round() as u32);
assert!(brightness(a_px2) > brightness(at(&px2, 2, 2)) + 30, "the disarmed single-sample path must still render node a correctly");
}
#[test]
#[ignore]
fn long_thin_diagonal_edge_at_the_engines_default_distance_renders_with_continuous_coverage() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let mut graph: Graph<(), ()> = Graph::new();
let a = graph.push_node((), "a", "cat-a", 2.5);
let b = graph.push_node((), "b", "cat-b", 2.5);
graph.push_edge(a, b, 1.0, ());
let from = Vec3::new(-260.0, -140.0, 0.0);
let to = Vec3::new(260.0, 170.0, 0.0);
let particles = vec![Particle::at3(from.x, from.y, from.z), Particle::at3(to.x, to.y, to.z)];
let node_mesh = Arc::new(MeshLit::sphere(1.0, 8, 8, [1.0, 1.0, 1.0, 1.0]));
let edge_mesh = Arc::new(Mesh::unit_edge_quad([1.0, 1.0, 1.0, 1.0]));
let scene = render3d::build_scene(
&graph,
&particles,
&node_mesh,
&edge_mesh,
&HashSet::new(),
&render3d::Graph3DLighting::default(),
&render3d::Graph3DEdgeStyle::default(),
);
let d = 500.0f32;
let mut camera = PerspectiveCamera::new(Vec3::new(0.0, 0.0, d), Vec3::ZERO, W as f32 / H as f32);
camera.z_near = (d * 0.001).max(0.05);
camera.z_far = (d * 4.0).max(2_000.0);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
r.set_sample_count(&device, 4);
let (tex, view) = make_target(&device);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc, &view, &camera, &scene);
queue.submit(Some(enc.finish()));
let px = readback_rgba(&device, &queue, &tex);
let bg = brightness(at(&px, 2, 2));
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let delta = to - from;
let steps = 200;
let margin = 0.08;
let mut lit_flags: Vec<bool> = Vec::with_capacity(steps + 1);
for i in 0..=steps {
let t = margin + (1.0 - 2.0 * margin) * (i as f32 / steps as f32);
let p = from + delta * t;
let lit = match project_world_to_screen(&camera, p, viewport) {
Some((sx, sy)) if sx >= 2.0 && sy >= 2.0 && sx < (W - 2) as f64 && sy < (H - 2) as f64 => {
let px_here = brightest_near(&px, sx.round() as u32, sy.round() as u32, 1);
brightness(px_here) > bg + 15
}
_ => false,
};
lit_flags.push(lit);
}
let lit_count = lit_flags.iter().filter(|&&l| l).count();
let mut max_gap = 0usize;
let mut cur_gap = 0usize;
for &lit in &lit_flags {
if lit {
cur_gap = 0;
} else {
cur_gap += 1;
max_gap = max_gap.max(cur_gap);
}
}
eprintln!("long thin diagonal edge: samples={} lit={lit_count} max_gap={max_gap}", lit_flags.len());
assert!(
(lit_count as f32 / lit_flags.len() as f32) > 0.95,
"the edge-quad pipeline must render CONTINUOUS coverage along a diagonal edge at the engine's own default distance — only {lit_count}/{} samples were lit (the ORIGINAL cylinder-edge path left only ~4% lit on this exact fixture, see the divergence log)",
lit_flags.len()
);
assert!(
max_gap <= 2,
"no run of more than 2 consecutive dark samples is allowed — a longer run IS the dotted/stippled defect this wave fixed; max_gap={max_gap}"
);
}
#[test]
#[ignore]
fn edge_quad_analytic_aa_feathers_the_line_edge_instead_of_a_binary_hard_step() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let mut graph: Graph<(), ()> = Graph::new();
let a = graph.push_node((), "a", "cat-a", 1.0);
let b = graph.push_node((), "b", "cat-b", 1.0);
graph.push_edge(a, b, 1.0, ());
let particles = vec![Particle::at3(-10.0, 0.0, 0.0), Particle::at3(10.0, 0.0, 0.0)];
let edge_mesh = Arc::new(Mesh::unit_edge_quad([1.0, 1.0, 1.0, 1.0]));
let edges = render3d::build_edge_instances(&graph, &particles, &edge_mesh, &HashSet::new(), &render3d::Graph3DEdgeStyle::default());
assert_eq!(edges.len(), 1, "exactly one edge, no node spheres, in this scene");
let mut scene = Scene3D::new();
scene.nodes = edges;
let d = 60.0f32;
let mut camera = PerspectiveCamera::new(Vec3::new(0.0, 0.0, d), Vec3::ZERO, W as f32 / H as f32);
camera.z_near = (d * 0.001).max(0.05);
camera.z_far = (d * 4.0).max(2_000.0);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
r.set_bloom_strength(0.0);
r.set_ssao_strength(0.0);
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let (mx, my) = project_world_to_screen(&camera, Vec3::ZERO, viewport).expect("edge midpoint is in front of the eye");
let cx = (mx.round() as i64).clamp(0, (W - 1) as i64) as u32;
let cy = my.round() as i64;
let mut positive_side_found = false;
let mut negative_side_found = false;
let mut last_bg = 0u32;
for i in 0..12 {
let width_px = 1.75 + i as f32 * 0.4;
r.set_edge_width_px(width_px);
let (tex, view) = make_target(&device);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc, &view, &camera, &scene);
queue.submit(Some(enc.finish()));
let px = readback_rgba(&device, &queue, &tex);
let bg = brightness(at(&px, 2, 2));
last_bg = bg;
let sweep: Vec<(i64, u32)> = (-10..=10)
.map(|dy: i64| {
let y = (cy + dy).clamp(0, (H - 1) as i64) as u32;
(dy, brightness(at(&px, cx, y)))
})
.collect();
let (peak_dy, core) = *sweep.iter().max_by_key(|(_, b)| *b).expect("sweep is non-empty");
if core <= bg + 20 {
eprintln!("width_px={width_px:.2}: line not clearly visible (core={core} bg={bg}) — skipping this width");
continue;
}
let lo = bg + 8;
let hi = core.saturating_sub(8);
if lo >= hi {
continue;
}
let pos_here = (1..=3).any(|off| {
sweep.iter().find(|(dy, _)| *dy == peak_dy + off).is_some_and(|(_, b)| *b > lo && *b < hi)
});
let neg_here = (1..=3).any(|off| {
sweep.iter().find(|(dy, _)| *dy == peak_dy - off).is_some_and(|(_, b)| *b > lo && *b < hi)
});
eprintln!("width_px={width_px:.2} peak_dy={peak_dy} core={core} bg={bg} pos_intermediate={pos_here} neg_intermediate={neg_here} sweep={sweep:?}");
positive_side_found |= pos_here;
negative_side_found |= neg_here;
if positive_side_found && negative_side_found {
break;
}
}
assert!(
positive_side_found,
"across a sweep of on-screen line widths, at least one configuration must show an INTERMEDIATE (partially-covered) brightness sample on the +dy side of the edge's centerline — a binary hard edge would NEVER produce one at ANY width/phase; bg={last_bg}"
);
assert!(
negative_side_found,
"across a sweep of on-screen line widths, at least one configuration must show an INTERMEDIATE (partially-covered) brightness sample on the -dy side of the edge's centerline — a binary hard edge would NEVER produce one at ANY width/phase; bg={last_bg}"
);
}
#[test]
#[ignore]
fn per_instance_edge_width_makes_a_higher_weight_edge_read_wider_in_pixels() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let edge_mesh = Arc::new(Mesh::unit_edge_quad([1.0, 1.0, 1.0, 1.0]));
let mut thin_graph: Graph<(), ()> = Graph::new();
let ta = thin_graph.push_node((), "a", "cat-a", 1.0);
let tb = thin_graph.push_node((), "b", "cat-b", 1.0);
thin_graph.push_edge(ta, tb, 1.0, ());
let thin_particles = vec![Particle::at3(-10.0, -6.0, 0.0), Particle::at3(10.0, -6.0, 0.0)];
let thin_edges =
render3d::build_edge_instances(&thin_graph, &thin_particles, &edge_mesh, &HashSet::new(), &render3d::Graph3DEdgeStyle::default());
assert_eq!(thin_edges.len(), 1);
let base_width_px = 1.75_f32;
let target_scale = 5.0_f32 / base_width_px;
let thick_weight = (2.0 * target_scale - 1.0).powi(2);
let mut thick_graph: Graph<(), ()> = Graph::new();
let ca = thick_graph.push_node((), "a", "cat-a", 1.0);
let cb = thick_graph.push_node((), "b", "cat-b", 1.0);
thick_graph.push_edge(ca, cb, thick_weight, ());
let thick_particles = vec![Particle::at3(-10.0, 6.0, 0.0), Particle::at3(10.0, 6.0, 0.0)];
let thick_edges =
render3d::build_edge_instances(&thick_graph, &thick_particles, &edge_mesh, &HashSet::new(), &render3d::Graph3DEdgeStyle::default());
assert_eq!(thick_edges.len(), 1);
let mut scene = Scene3D::new();
scene.nodes = thin_edges;
scene.nodes.extend(thick_edges);
let d = 60.0f32;
let mut camera = PerspectiveCamera::new(Vec3::new(0.0, 0.0, d), Vec3::ZERO, W as f32 / H as f32);
camera.z_near = (d * 0.001).max(0.05);
camera.z_far = (d * 4.0).max(2_000.0);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
r.set_bloom_strength(0.0);
r.set_ssao_strength(0.0);
let (tex, view) = make_target(&device);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc, &view, &camera, &scene);
queue.submit(Some(enc.finish()));
let px = readback_rgba(&device, &queue, &tex);
let bg = brightness(at(&px, 2, 2));
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let (thin_mx, thin_my) = project_world_to_screen(&camera, Vec3::new(0.0, -6.0, 0.0), viewport).expect("thin edge midpoint is in front of the eye");
let (thick_mx, thick_my) = project_world_to_screen(&camera, Vec3::new(0.0, 6.0, 0.0), viewport).expect("thick edge midpoint is in front of the eye");
let measure_span = |cx: f64, cy: f64| -> usize {
let cxi = (cx.round() as i64).clamp(0, (W - 1) as i64) as u32;
let cyi = cy.round() as i64;
(-15..=15)
.filter(|dy| {
let y = (cyi + dy).clamp(0, (H - 1) as i64) as u32;
brightness(at(&px, cxi, y)) > bg + 20
})
.count()
};
let thin_span = measure_span(thin_mx, thin_my);
let thick_span = measure_span(thick_mx, thick_my);
eprintln!("thin_span={thin_span} thick_span={thick_span} (thick_weight={thick_weight:.2})");
assert!(thin_span > 0, "the thin (weight 1.0, today's default 1.75px) edge must be visible at all");
assert!(
thick_span > thin_span,
"the higher-weight edge's per-instance width must read visibly wider in pixels than the default-weight edge: thin_span={thin_span} thick_span={thick_span}"
);
}
#[test]
#[ignore]
fn id_pass_scene_produces_exactly_decodable_node_ids_at_known_pixels() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let engine = head_on_engine();
let id_scene = engine.build_id_pass_scene();
assert_eq!(id_scene.nodes.len(), 2, "the id-pass is node-only — no edge lines");
assert_eq!(id_scene.clear_color, [1.0, 1.0, 1.0, 1.0], "the id-pass background must be the reserved white sentinel");
let aspect = W as f32 / H as f32;
let camera = engine.camera(aspect);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
r.set_bloom_strength(0.0);
r.set_ssao_strength(0.0);
let (tex, view) = make_target(&device);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
r.render(&device, &queue, &mut enc, &view, &camera, &id_scene);
queue.submit(Some(enc.finish()));
let px = readback_rgba(&device, &queue, &tex);
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let (ax, ay) = project_world_to_screen(&camera, Vec3::new(-6.0, 0.0, 0.0), viewport).expect("node a is in front of the eye");
let (bx, by) = project_world_to_screen(&camera, Vec3::new(6.0, 0.0, 0.0), viewport).expect("node b is in front of the eye");
let a_px = at(&px, ax.round() as u32, ay.round() as u32);
let b_px = at(&px, bx.round() as u32, by.round() as u32);
let corner_px = at(&px, 2, 2);
eprintln!("a={a_px:?} b={b_px:?} corner={corner_px:?}");
assert_eq!(
render3d::decode_gpu_pick_pixel(a_px, 2),
Some(NodeIndex(0)),
"node a's own pixel must decode back to its exact NodeIndex through the real render pipeline"
);
assert_eq!(
render3d::decode_gpu_pick_pixel(b_px, 2),
Some(NodeIndex(1)),
"node b's own pixel must decode back to its exact NodeIndex through the real render pipeline"
);
assert_eq!(
render3d::decode_gpu_pick_pixel(corner_px, 2),
None,
"a background pixel (white clear color, tonemapped) must decode to no hit, never a node"
);
}
#[test]
#[ignore]
fn request_and_poll_gpu_pick_resolves_the_correct_node_via_deferred_readback() {
let Some((device, queue)) = init_device() else {
eprintln!("no GPU adapter; skipping");
return;
};
let engine = head_on_engine();
let id_scene = engine.build_id_pass_scene();
let aspect = W as f32 / H as f32;
let camera = engine.camera(aspect);
let mut r = Renderer3D::new(&device, &queue, COLOR_FORMAT, (W, H), 64);
let viewport = Rect::new(0.0, 0.0, W as f64, H as f64);
let (ax, ay) = project_world_to_screen(&camera, Vec3::new(-6.0, 0.0, 0.0), viewport).expect("node a is in front of the eye");
let readback = pick3d::request_gpu_pick(&device, &queue, &mut r, &id_scene, &camera, (W, H), (ax, ay));
let mut result = None;
for _ in 0..10_000 {
match pick3d::poll_gpu_pick(&device, &readback, 2) {
Some(r) => {
result = Some(r);
break;
}
None => std::hint::spin_loop(),
}
}
assert_eq!(
result,
Some(Some(NodeIndex(0))),
"the deferred GPU pick must resolve to node a's own NodeIndex at its exact screen position"
);
}