142 lines
7.1 KiB
Rust
142 lines
7.1 KiB
Rust
use egui::{Rect, pos2};
|
|
use serde_json::json;
|
|
use solidcraft_engine::geom::Vec3;
|
|
use solidcraft_gpu::pick::{PickTile, TILE, tile_around};
|
|
|
|
use super::*;
|
|
use crate::viewport::{Hit, pick, projection};
|
|
|
|
fn app() -> SolveApp {
|
|
let mut s = solidcraft_engine::Session::default();
|
|
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
|
s.execute("solid.fillet", &json!({"edges": [[40, 15, 20]], "radius": 4})).unwrap();
|
|
s.execute("solid.cylinder", &json!({"radius": 8, "height": 35, "base": [10, 10, 0], "operation": "new_body"})).unwrap();
|
|
let mut app = SolveApp::new(s, crate::Services::default());
|
|
app.ui.show_origin = false;
|
|
app.cam.target = Vec3::new(20.0, 15.0, 12.0);
|
|
app.cam.distance = 110.0;
|
|
// A GPU viewport (no device is needed to hand the picker a tile).
|
|
app.viewport.gpu = Some(crate::gpu::GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
|
app
|
|
}
|
|
|
|
const RECT: Rect = Rect { min: pos2(0.0, 0.0), max: pos2(640.0, 480.0) };
|
|
|
|
/// Installs the pass's scene, as `paint` makes it, and returns the view's key.
|
|
fn install_scene(app: &SolveApp, proj: &Proj) -> u64 {
|
|
let st = app.session.world_state();
|
|
let model = model_key(app, &st);
|
|
let s = scene(app, &st, model).unwrap();
|
|
*app.viewport.pick_buffer.scene.lock().unwrap() = Some(s);
|
|
view_key(app, proj, model)
|
|
}
|
|
|
|
/// A tile only narrows the edges: the edge it names near the cursor is picked as the CPU picks
|
|
/// it, an edge it does not name (hidden there) is not, and without a tile the CPU indexes answer
|
|
/// and a tile is asked for.
|
|
#[test]
|
|
fn a_tile_names_the_edges_the_picker_tests() {
|
|
let app = app();
|
|
let proj = projection(&app, RECT);
|
|
// A point on the box's top front edge (y = 0, z = 20).
|
|
let at = proj.to_screen(Vec3::new(20.0, 0.0, 20.0)).unwrap();
|
|
let cpu = pick(&app, &proj, at);
|
|
let Some(Hit::Edge { index, body, .. }) = cpu.iter().find(|h| matches!(h, Hit::Edge { .. })).cloned() else { panic!("{cpu:?}") };
|
|
assert!(app.viewport.pick_buffer.want.lock().unwrap().is_some(), "no tile yet: one is asked for");
|
|
let key = install_scene(&app, &proj);
|
|
let st = app.session.world_state();
|
|
let bi = st.bodies.iter().position(|b| b.name == body).unwrap();
|
|
let s = app.viewport.pick_buffer.scene.lock().unwrap().clone().unwrap();
|
|
let id = s.edges.iter().position(|&(b, e)| b as usize == bi && e as usize == index).unwrap() as u32 + 1;
|
|
let size = view_size(&app, &proj);
|
|
let (origin, tsize) = tile_around([at.x as u32, at.y as u32], size);
|
|
let tile = |ids: Vec<u32>| PickTile { key, origin, size: tsize, ids };
|
|
let n = (tsize[0] * tsize[1]) as usize;
|
|
// The edge's id at the cursor's pixel.
|
|
let mut ids = vec![0; n];
|
|
ids[((at.y as u32 - origin[1]) * tsize[0] + (at.x as u32 - origin[0])) as usize] = id;
|
|
app.viewport.pick_buffer.out.lock().unwrap().tiles = vec![tile(ids)];
|
|
assert_eq!(pick(&app, &proj, at), cpu, "the named edge is picked as the CPU picks it");
|
|
// A tile naming no edge there: no edge or vertex is picked.
|
|
app.viewport.pick_buffer.out.lock().unwrap().tiles = vec![tile(vec![0; n])];
|
|
let none = pick(&app, &proj, at);
|
|
assert!(!none.iter().any(|h| matches!(h, Hit::Edge { .. } | Hit::Vertex { .. })), "{none:?}");
|
|
assert!(none.iter().any(|h| matches!(h, Hit::Face { .. })), "faces are still picked: {none:?}");
|
|
// A tile of another view does not answer.
|
|
app.viewport.pick_buffer.out.lock().unwrap().tiles = vec![PickTile { key: key ^ 1, ..tile(vec![0; n]) }];
|
|
assert_eq!(pick(&app, &proj, at), cpu);
|
|
// Wireframe: nothing hides an edge, and the buffer is not used.
|
|
let mut wire = app;
|
|
wire.ui.visual_style = 2;
|
|
wire.viewport.pick_buffer.out.lock().unwrap().tiles = vec![tile(vec![0; n])];
|
|
assert!(edges_near(&wire, &proj, at, &wire.session.world_state()).is_none());
|
|
}
|
|
|
|
/// On a real device: the pass drawn for the view, and picks over a grid of the view helped by its
|
|
/// tiles answer as the CPU picks (an edge's pixels and the CPU's visibility test can disagree by
|
|
/// a pixel at a silhouette, so a few may differ). Skipped without a GPU adapter.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[test]
|
|
fn picks_through_the_pass_match_the_cpu() {
|
|
let Some((_, device, queue)) = crate::gpu::offscreen_device() else {
|
|
assert!(std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_none(), "a GPU is required here");
|
|
eprintln!("skipping: no GPU adapter");
|
|
return;
|
|
};
|
|
let mut app = app();
|
|
for (perspective, yaw, pitch) in [(false, 0.6, 0.5), (true, 2.4, 0.3)] {
|
|
app.ui.perspective = perspective;
|
|
app.cam.yaw = yaw;
|
|
app.cam.pitch = pitch;
|
|
let proj = projection(&app, RECT);
|
|
let key = install_scene(&app, &proj);
|
|
let s = app.viewport.pick_buffer.scene.lock().unwrap().clone().unwrap();
|
|
let e = proj.cam.eye().to_f32();
|
|
let view = PickView {
|
|
key,
|
|
view_proj: proj.vp.to_f32(),
|
|
eye: [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }],
|
|
back: proj.cam.back().to_f32(),
|
|
lift: lift(&app) as f32,
|
|
size: view_size(&app, &proj),
|
|
};
|
|
let mut pb = PickBuffer::new(&device);
|
|
let out = app.viewport.pick_buffer.out.clone();
|
|
// Tiles over the whole view.
|
|
let mut wanted = Vec::new();
|
|
for ty in (0..view.size[1]).step_by(TILE as usize - 32) {
|
|
for tx in (0..view.size[0]).step_by(TILE as usize - 32) {
|
|
wanted.push([tx + TILE / 2, ty + TILE / 2]);
|
|
}
|
|
}
|
|
let mut tiles = Vec::new();
|
|
for c in wanted {
|
|
for _ in 0..6 {
|
|
let mut encoder = device.create_command_encoder(&Default::default());
|
|
pb.prepare(&device, &queue, &mut encoder, s.key, &s.scene, &view, Some(c), &out);
|
|
queue.submit([encoder.finish()]);
|
|
let _ = device.poll(egui_wgpu::wgpu::PollType::wait_indefinitely());
|
|
}
|
|
tiles.append(&mut out.lock().unwrap().tiles);
|
|
}
|
|
let (mut same, mut differ, mut edges) = (0, 0, 0);
|
|
for j in 0..46 {
|
|
for i in 0..62 {
|
|
let p = pos2(10.0 + i as f32 * 10.0 + (j % 3) as f32, 10.0 + j as f32 * 10.0);
|
|
let r = 8i64;
|
|
let w = [p.x as i64 - r, p.y as i64 - r, p.x as i64 + r, p.y as i64 + r];
|
|
let Some(t) = tiles.iter().find(|t| t.covers(key, view.size, w)).cloned() else { continue };
|
|
out.lock().unwrap().tiles = vec![t];
|
|
let through = pick(&app, &proj, p);
|
|
out.lock().unwrap().tiles.clear();
|
|
let cpu = pick(&app, &proj, p);
|
|
edges += usize::from(cpu.iter().any(|h| matches!(h, Hit::Edge { .. })));
|
|
if through == cpu { same += 1 } else { differ += 1 }
|
|
}
|
|
}
|
|
eprintln!("perspective {perspective}: {same} same, {differ} differ, {edges} with an edge");
|
|
assert!(edges > 100, "the grid reaches edges ({edges})");
|
|
assert!(differ * 100 <= same + differ, "{differ} of {} picks differ", same + differ);
|
|
}
|
|
}
|