Headless pictures draw on the GPU, with the viewport's pipelines (CPU rasterizer as last tier) (#1555)
Thanks @jyviko.
This commit is contained in:
1 parent
48322f6464
commit
1a0b33fcf3
54 files changed
+3525
-1998
No files matched your search
+68
-150
@@ -10,7 +10,7 @@ use solidcraft_engine::sketch::CurveKind;
|
||||
use solidcraft_engine::view::{colors, grid_step, sketch_lines_with_sampler};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use crate::gpu::{GpuScene, GpuTarget, SceneSlot, ViewportCallback};
|
||||
use crate::gpu::{Frame, GpuScene, GpuTarget, SceneSlot, ViewportCallback};
|
||||
use crate::navigation::{CameraInput, NavCaps, OrbitSnap, orbit_drag};
|
||||
use crate::selection::{BoxSel, origin_quads, origin_size, ray_quad};
|
||||
use crate::theme::Tokens;
|
||||
@@ -69,9 +69,6 @@ pub struct ViewportState {
|
||||
pub(crate) display: std::sync::Mutex<crate::sketch_display::DisplayCache>,
|
||||
hl_slot: SceneSlot,
|
||||
hl_key: u64,
|
||||
cpu: Option<(u64, egui::TextureHandle)>,
|
||||
/// Nothing will look at the frame (the headless scenario harness): skip the software render.
|
||||
pub no_pixels: bool,
|
||||
pub nav: Option<NavMode>,
|
||||
/// The preset's navigation for the left-button drag under way (Alias: Shift+Alt), taken
|
||||
/// when the button went down, so the drag neither selects nor moves sketch geometry.
|
||||
@@ -1501,20 +1498,9 @@ pub fn selection_bounds(app: &SolveApp) -> Option<solidcraft_engine::geom::Aabb3
|
||||
}
|
||||
|
||||
/// The section as the shader's clip plane (normal, d), zero when there is none.
|
||||
/// The surface analysis for the shader (see `gpu::ViewportCallback::analysis`).
|
||||
/// The surface analysis for the shader (see `gpu::Frame::analysis`).
|
||||
fn analysis_uniform(app: &SolveApp) -> [f32; 8] {
|
||||
use solidcraft_engine::SurfaceAnalysis as A;
|
||||
match app.session.analysis {
|
||||
Some(A::Zebra { stripes }) => [1.0, stripes as f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
Some(A::Draft { pull, angle }) => [2.0, angle as f32, 0.0, 0.0, pull.x as f32, pull.y as f32, pull.z as f32, 0.0],
|
||||
Some(A::Curvature { radius }) => [3.0, radius as f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
Some(A::Environment) => [4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
Some(A::Access { dir }) => {
|
||||
let (scale, size, eps) = crate::access::map(app).map(|m| (m.scale, m.size as f32, 1.5 / m.scale.max(1e-9))).unwrap_or((0.0, 0.0, 0.0));
|
||||
[5.0, eps, scale, size, dir.x as f32, dir.y as f32, dir.z as f32, 0.0]
|
||||
}
|
||||
None => [0.0; 8],
|
||||
}
|
||||
solidcraft_gpu::view::analysis_uniform(app.session.analysis.map(|a| a.mode()), crate::access::map(app).as_ref())
|
||||
}
|
||||
|
||||
/// While Draft, Accessibility or Curvature Map analysis is on, what each of its colours means and
|
||||
@@ -2193,7 +2179,8 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
update_refinement(app, ui.ctx(), &proj);
|
||||
let key = scene_key(app);
|
||||
let t0 = crate::now_ms();
|
||||
if let Some(_g) = app.viewport.gpu {
|
||||
// Drawn on the GPU; without one (the headless harness, tests) the model is not drawn.
|
||||
if app.viewport.gpu.is_some() {
|
||||
if key != app.viewport.key {
|
||||
let sc = build_scene(app);
|
||||
if let Ok(mut slot) = app.viewport.slot.lock() {
|
||||
@@ -2221,7 +2208,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let size_px = [rect.width() * ppp, rect.height() * ppp];
|
||||
let ssaa = supersampling(app, ui.ctx(), proj.vp.to_f32(), size_px);
|
||||
let cb = ViewportCallback {
|
||||
let cb = ViewportCallback(Frame {
|
||||
ssaa,
|
||||
product: false,
|
||||
key: app.viewport.key,
|
||||
@@ -2246,10 +2233,8 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let e = proj.cam.eye().to_f32();
|
||||
[e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }]
|
||||
},
|
||||
};
|
||||
});
|
||||
painter.add(egui_wgpu::Callback::new_paint_callback(rect, cb));
|
||||
} else {
|
||||
cpu_render(app, ui.ctx(), &painter, rect, &proj);
|
||||
}
|
||||
analysis_legend(app, &painter, rect);
|
||||
|
||||
@@ -2952,44 +2937,9 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter, rect: Rect, proj: &Proj) {
|
||||
if app.viewport.no_pixels {
|
||||
return;
|
||||
}
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
sketch_scene_key(app, proj).hash(&mut h);
|
||||
for v in [proj.cam.yaw, proj.cam.pitch, proj.cam.roll, proj.cam.distance, proj.cam.target.x, proj.cam.target.y, proj.cam.target.z] {
|
||||
v.to_bits().hash(&mut h);
|
||||
}
|
||||
(rect.width() as u32, rect.height() as u32).hash(&mut h);
|
||||
app.ui.dark.hash(&mut h);
|
||||
app.preferences.color_vision.hash(&mut h);
|
||||
let key = h.finish();
|
||||
if app.viewport.cpu.as_ref().is_none_or(|(k, _)| *k != key) {
|
||||
let mut budget = solidcraft_engine::render::SamplingBudget::new(131_072, 1_048_576);
|
||||
let palette = scene_palette(app.ui.dark, app.preferences.color_vision);
|
||||
let scene = solidcraft_engine::view::scene_with_palette(
|
||||
&app.session,
|
||||
&proj.cam,
|
||||
section_plane(app),
|
||||
|sk, index, plane| crate::sketch_display::curve_with_budget(sk, index, plane, proj, app.viewport.pixels_per_point(), &mut budget).parts,
|
||||
&palette,
|
||||
);
|
||||
let w = (rect.width() as usize).clamp(16, 4096);
|
||||
let hh = (rect.height() as usize).clamp(16, 4096);
|
||||
let c = solidcraft_engine::render::render(&scene, &proj.cam, w, hh);
|
||||
let img = egui::ColorImage::from_rgba_unmultiplied([c.w, c.h], &c.rgba);
|
||||
let tex = ctx.load_texture("sc_cpu_viewport", img, egui::TextureOptions::LINEAR);
|
||||
app.viewport.cpu = Some((key, tex));
|
||||
}
|
||||
if let Some((_, tex)) = &app.viewport.cpu {
|
||||
painter.image(tex.id(), rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
/// The colours of a CPU picture of the model (`ui.render` and the CPU viewport) in the dark or
|
||||
/// the light theme and a colour vision mode, from the tokens the window draws with, so the picture
|
||||
/// matches the window. The light theme's tokens (Off) are the engine's own colours, so a light
|
||||
/// The colours of a headless picture of the model (`ui.render`, the agent's view image) in the
|
||||
/// dark or the light theme and a colour vision mode, from the tokens the window draws with, so the
|
||||
/// picture matches the window. The light theme's tokens (Off) are the engine's own colours, so a light
|
||||
/// render is unchanged.
|
||||
pub(crate) fn scene_palette(dark: bool, mode: crate::theme::ColorVision) -> solidcraft_engine::view::Palette {
|
||||
let t = Tokens::themed(dark, mode);
|
||||
@@ -3011,6 +2961,7 @@ pub(crate) fn scene_palette(dark: bool, mode: crate::theme::ColorVision) -> soli
|
||||
sketch_centerline: rgb(t.sketch_centerline),
|
||||
axes: t.triad.map(rgb),
|
||||
cap: rgb(t.section_cap),
|
||||
analysis: t.analysis,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3337,7 +3288,6 @@ mod tests {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.run_script(&solidcraft_engine::sample::script()).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
const SIZE: u32 = 256;
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
@@ -3400,7 +3350,6 @@ mod tests {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.run_script(&solidcraft_engine::sample::script()).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
const SIZE: u32 = 256;
|
||||
@@ -3439,7 +3388,6 @@ mod tests {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.run_script(&solidcraft_engine::sample::script()).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
||||
const SIZE: u32 = 256;
|
||||
@@ -3498,7 +3446,6 @@ mod tests {
|
||||
s.execute("solid.box", &json!({"length": 16, "width": 16, "height": 20, "corner": [12, 12, 20], "operation": "cut"})).unwrap();
|
||||
s.execute("solid.cylinder", &json!({"base": [33, 33, 40], "diameter": 10, "height": 10, "operation": "join"})).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
const SIZE: u32 = 256;
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
@@ -3599,13 +3546,13 @@ mod tests {
|
||||
crate::sketch_palette::set_mode(&mut h.app, 0);
|
||||
}
|
||||
|
||||
/// The CPU pictures (`ui.render`, the CPU viewport) are drawn in the window's theme (#974)
|
||||
/// and colour vision mode (#778). The light theme's tokens are the engine's own colours, so a
|
||||
/// Headless pictures of the view (`ui.render`, the agent's view image) are drawn in the
|
||||
/// window's theme (#974) and colour vision mode (#778). The light theme's tokens are the engine's own colours, so a
|
||||
/// light picture in Off is unchanged; otherwise each part takes the token the window draws it
|
||||
/// with. Sketch states follow what the window does with them (`sketch_color`) and the axes the
|
||||
/// mode's triad, so the two can't drift apart.
|
||||
#[test]
|
||||
fn cpu_pictures_are_drawn_in_the_windows_theme() {
|
||||
fn pictures_are_drawn_in_the_windows_theme() {
|
||||
use crate::theme::ColorVision;
|
||||
use solidcraft_engine::view::Palette;
|
||||
assert_eq!(scene_palette(false, ColorVision::Off), Palette::DEFAULT, "the light theme is the engine's colours");
|
||||
@@ -3620,6 +3567,7 @@ mod tests {
|
||||
for (dark, mode) in [false, true].into_iter().flat_map(|d| ColorVision::ALL.map(|m| (d, m))) {
|
||||
let (p, t) = (scene_palette(dark, mode), Tokens::themed(dark, mode));
|
||||
assert_eq!(p.axes, t.triad.map(rgb), "dark {dark}, {mode:?}: the axes");
|
||||
assert_eq!(p.analysis, t.analysis, "dark {dark}, {mode:?}: the surface analysis");
|
||||
for (tag, cpu) in [
|
||||
(colors::SKETCH, p.sketch),
|
||||
(colors::SKETCH_FIXED, p.sketch_fixed),
|
||||
@@ -3636,65 +3584,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The CPU viewport follows a theme or colour vision mode change at once: both are part of its
|
||||
/// picture's key.
|
||||
/// The view's picture follows a theme change: its background is the window's.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn cpu_viewport_redraws_in_the_new_theme() {
|
||||
fn the_view_picture_follows_the_theme() {
|
||||
if !crate::gpu::test_picture_renderer() {
|
||||
return;
|
||||
}
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("solid.box", json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(160.0, 120.0));
|
||||
let cam = solidcraft_engine::view::home_camera(&h.app.session);
|
||||
let proj = Proj { cam, vp: cam.view_proj(4.0 / 3.0, 100.0), rect };
|
||||
h.app.viewport.no_pixels = false;
|
||||
// The new picture's top row, most common colour first (the background's top); None when
|
||||
// nothing new was drawn.
|
||||
let top_row = |h: &mut crate::scenario::Harness| {
|
||||
let mut out = h.ctx.run_ui(egui::RawInput { screen_rect: Some(rect), ..Default::default() }, |ui| {
|
||||
cpu_render(&mut h.app, ui.ctx(), ui.painter(), rect, &proj);
|
||||
});
|
||||
let id = h.app.viewport.cpu.as_ref().map(|(_, tex)| tex.id());
|
||||
let top = id.and_then(|id| out.textures_delta.set.get(&id)).and_then(|d| d.iter().find(|delta| delta.pos.is_none())).map(|upload| {
|
||||
let egui::ImageData::Color(image) = &upload.image;
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for px in image.pixels.iter().take(image.size[0]) {
|
||||
*counts.entry(*px).or_insert(0) += 1;
|
||||
}
|
||||
counts.into_iter().max_by_key(|(_, n)| *n).map(|(c, _)| c).unwrap_or_default()
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
top
|
||||
};
|
||||
h.app.cam = solidcraft_engine::view::home_camera(&h.app.session);
|
||||
for dark in [false, true, false] {
|
||||
h.app.ui.dark = dark;
|
||||
let Some(px) = top_row(&mut h) else { panic!("dark {dark}: the CPU viewport kept the old picture") };
|
||||
let img = crate::control::view_rgba(&h.app, 160, 120).unwrap();
|
||||
// The top row's most common colour: the background's top.
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for px in img.as_chunks::<4>().0.iter().take(160) {
|
||||
*counts.entry([px[0], px[1], px[2]]).or_insert(0) += 1;
|
||||
}
|
||||
let px = counts.into_iter().max_by_key(|(_, n)| *n).map(|(c, _)| c).unwrap_or_default();
|
||||
let want = if dark { Tokens::dark() } else { Tokens::light() }.viewport_top;
|
||||
let near = [px.r(), px.g(), px.b()].iter().zip([want.r(), want.g(), want.b()]).all(|(a, b)| a.abs_diff(b) <= 2);
|
||||
let near = px.iter().zip([want.r(), want.g(), want.b()]).all(|(a, b)| a.abs_diff(b) <= 2);
|
||||
assert!(near, "dark {dark}: top row {px:?}, the window's background {want:?}");
|
||||
}
|
||||
// And a colour vision mode change, on its own.
|
||||
h.app.preferences.color_vision = crate::theme::ColorVision::Deutan;
|
||||
assert!(top_row(&mut h).is_some(), "a new mode kept the old picture");
|
||||
assert!(top_row(&mut h).is_none(), "no change, no new picture");
|
||||
}
|
||||
|
||||
/// The view's picture is cut by the section the viewport shows: an open Section Analysis
|
||||
/// dialog's preview, a sketch's Slice.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn cpu_viewport_uses_section_preview_and_sketch_slice() {
|
||||
fn pixels(h: &mut crate::scenario::Harness, cam: Camera) -> Vec<Color32> {
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(320.0, 240.0));
|
||||
let proj = Proj { cam, vp: cam.view_proj(4.0 / 3.0, 100.0), rect };
|
||||
h.app.viewport.no_pixels = false;
|
||||
// Force a texture upload so this test reads the actual CPU rasterizer result.
|
||||
h.app.viewport.cpu = None;
|
||||
let mut out = h.ctx.run_ui(egui::RawInput { screen_rect: Some(rect), ..Default::default() }, |ui| {
|
||||
cpu_render(&mut h.app, ui.ctx(), ui.painter(), rect, &proj);
|
||||
});
|
||||
let id = h.app.viewport.cpu.as_ref().unwrap().1.id();
|
||||
let upload = out.textures_delta.set.get(&id).unwrap().iter().find(|delta| delta.pos.is_none()).unwrap();
|
||||
let egui::ImageData::Color(image) = &upload.image;
|
||||
assert_eq!(image.size, [320, 240]);
|
||||
let pixels = image.pixels.clone();
|
||||
out.textures_delta.clear();
|
||||
pixels
|
||||
fn the_view_picture_uses_section_preview_and_sketch_slice() {
|
||||
fn pixels(h: &mut crate::scenario::Harness, cam: Camera) -> Vec<[u8; 4]> {
|
||||
h.app.cam = cam;
|
||||
let img = crate::control::view_rgba(&h.app, 320, 240).unwrap();
|
||||
img.as_chunks::<4>().0.to_vec()
|
||||
}
|
||||
if !crate::gpu::test_picture_renderer() {
|
||||
return;
|
||||
}
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("solid.box", json!({"length":40,"width":30,"height":20})).unwrap();
|
||||
@@ -3708,12 +3634,12 @@ mod tests {
|
||||
*offset = "15 mm".into();
|
||||
assert!(h.app.session.section.is_none());
|
||||
let preview = pixels(&mut h, cam);
|
||||
assert!(whole.iter().zip(&preview).filter(|(a, b)| a != b).count() > 100, "CPU viewport ignored uncommitted section preview");
|
||||
assert!(whole.iter().zip(&preview).filter(|(a, b)| a != b).count() > 100, "the picture ignored the uncommitted section preview");
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &h.app.session.doc));
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
h.app.run("inspect.section", json!({"plane":"XZ","offset":15})).unwrap();
|
||||
h.app.dialog = None;
|
||||
assert_eq!(preview, pixels(&mut h, cam), "preview and committed section must rasterize identically");
|
||||
assert_eq!(preview, pixels(&mut h, cam), "preview and committed section must draw identically");
|
||||
|
||||
h.app.session.section = None;
|
||||
h.app.run("sketch.create", json!({"plane":{"origin":[0,0,10],"x_dir":[1,0,0],"y_dir":[0,1,0]}})).unwrap();
|
||||
@@ -3721,7 +3647,7 @@ mod tests {
|
||||
h.app.run("sketch.options", json!({"slice":true})).unwrap();
|
||||
assert!(h.app.session.section.is_none());
|
||||
let sliced = pixels(&mut h, cam);
|
||||
assert!(whole.iter().zip(&sliced).filter(|(a, b)| a != b).count() > 100, "CPU viewport ignored active Sketch Slice");
|
||||
assert!(whole.iter().zip(&sliced).filter(|(a, b)| a != b).count() > 100, "the picture ignored the active Sketch Slice");
|
||||
h.app.run("sketch.options", json!({"slice":false})).unwrap();
|
||||
assert_eq!(whole, pixels(&mut h, cam), "disabling Slice restores the whole-body pixels");
|
||||
}
|
||||
@@ -3876,7 +3802,6 @@ mod tests {
|
||||
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
||||
s.execute("sketch.point", &json!({"point": [0, 0]})).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
if gpu {
|
||||
// The callback is queued by egui; no device is needed to inspect scene updates.
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
||||
@@ -4106,7 +4031,6 @@ mod tests {
|
||||
s.selection = vec![Sel::Body { name: "Body1".into() }];
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.preferences.nav = "touchpad".into();
|
||||
app.viewport.no_pixels = true;
|
||||
app.start("solid.move");
|
||||
app.dialog.as_mut().unwrap().focus = false;
|
||||
let p = pos2(100.0, 100.0);
|
||||
@@ -4341,7 +4265,6 @@ mod tests {
|
||||
};
|
||||
for tess in [true, false] {
|
||||
let mut app = cylinder_app();
|
||||
app.viewport.no_pixels = true;
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
let body = app.session.world_state().bodies[0].clone();
|
||||
let before = build_scene(&app).merged();
|
||||
@@ -4444,8 +4367,9 @@ mod tests {
|
||||
if wireframe {
|
||||
scene.meshes.clear();
|
||||
}
|
||||
let png = solidcraft_engine::render::render_png(&scene, &cam, 1200, 800).unwrap();
|
||||
std::fs::write(std::path::Path::new(&directory).join(format!("smooth-ring-{name}.png")), png).unwrap();
|
||||
assert!(crate::gpu::test_picture_renderer(), "SMOOTH_SCREENSHOTS needs a GPU");
|
||||
let png = solidcraft_engine::capture::png(&scene, &cam, 1200, 800).unwrap();
|
||||
std::fs::write(std::path::Path::new(&directory).join(format!("smooth-ring-{name}.png")), png.bytes).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4846,7 +4770,6 @@ mod tests {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
assert!(app.preferences.trackpad_pan);
|
||||
app.viewport.no_pixels = true;
|
||||
crate::help::run(&mut app, &ctx, "shortcuts");
|
||||
let mut time = 0.0;
|
||||
let pan =
|
||||
@@ -4905,7 +4828,6 @@ mod tests {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
app.preferences.smooth_wheel_zoom = smooth;
|
||||
app.preferences.trackpad_pan = false;
|
||||
app.viewport.no_pixels = true;
|
||||
crate::help::run(&mut app, &ctx, "shortcuts");
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut SolveApp, pointer: Option<Pos2>, wheel: f32| {
|
||||
@@ -5396,7 +5318,6 @@ mod tests {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.run_script(&solidcraft_engine::sample::script()).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
app.run("inspect.draft", json!({"pull": [-1, -1, 0]})).unwrap();
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
||||
@@ -5448,46 +5369,39 @@ mod tests {
|
||||
assert!(changed.iter().all(|&i| b[4 * i..4 * i + 3].iter().zip(&c[4 * i..4 * i + 3]).all(|(x, y)| x <= y)), "stripes are darker");
|
||||
}
|
||||
|
||||
/// The CPU viewport draws in the colour vision mode: a fixed sketch line in the light theme
|
||||
/// The view's picture draws in the colour vision mode: a fixed sketch line in the light theme
|
||||
/// takes Deutan's value, and Off's is gone.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn cpu_viewport_draws_in_the_colour_vision_mode() {
|
||||
fn the_view_picture_draws_in_the_colour_vision_mode() {
|
||||
use crate::theme::ColorVision;
|
||||
if !crate::gpu::test_picture_renderer() {
|
||||
return;
|
||||
}
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.ui.dark = false;
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
let made = h.app.run("sketch.line", json!({"points": [[-30, -20], [30, 20]]})).unwrap();
|
||||
h.app.run("sketch.constraint.fix", json!({"entity": made["created"]["curves"][0]})).unwrap();
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(320.0, 240.0));
|
||||
let mut cam = solidcraft_engine::view::home_camera(&h.app.session);
|
||||
cam.set_view(solidcraft_engine::render::StandardView::Top);
|
||||
let proj = Proj { cam, vp: cam.view_proj(4.0 / 3.0, 100.0), rect };
|
||||
h.app.viewport.no_pixels = false;
|
||||
let count = |h: &mut crate::scenario::Harness, c: Color32| {
|
||||
h.app.viewport.cpu = None;
|
||||
let mut out = h.ctx.run_ui(egui::RawInput { screen_rect: Some(rect), ..Default::default() }, |ui| {
|
||||
cpu_render(&mut h.app, ui.ctx(), ui.painter(), rect, &proj);
|
||||
});
|
||||
let id = h.app.viewport.cpu.as_ref().map(|(_, tex)| tex.id());
|
||||
let n = id.and_then(|id| out.textures_delta.set.get(&id)).and_then(|d| d.iter().find(|delta| delta.pos.is_none())).map_or(0, |upload| {
|
||||
let egui::ImageData::Color(image) = &upload.image;
|
||||
image.pixels.iter().filter(|p| [p.r(), p.g(), p.b()].iter().zip([c.r(), c.g(), c.b()]).all(|(a, b)| a.abs_diff(b) <= 8)).count()
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
n
|
||||
h.app.cam = cam;
|
||||
let count = |h: &crate::scenario::Harness, c: Color32| {
|
||||
let img = crate::control::view_rgba(&h.app, 320, 240).unwrap();
|
||||
img.as_chunks::<4>().0.iter().filter(|p| p[..3].iter().zip([c.r(), c.g(), c.b()]).all(|(a, b)| a.abs_diff(b) <= 8)).count()
|
||||
};
|
||||
let (off, deutan) = (Tokens::light().sketch_locked, Tokens::themed(false, ColorVision::Deutan).sketch_locked);
|
||||
let (a, b) = (count(&mut h, off), count(&mut h, deutan));
|
||||
let (a, b) = (count(&h, off), count(&h, deutan));
|
||||
assert!(a > 20 && b == 0, "Off: {a} fixed-line pixels, {b} in Deutan's colour");
|
||||
h.app.preferences.color_vision = ColorVision::Deutan;
|
||||
let (a, b) = (count(&mut h, off), count(&mut h, deutan));
|
||||
let (a, b) = (count(&h, off), count(&h, deutan));
|
||||
assert!(a == 0 && b > 20, "Deutan: {a} fixed-line pixels in Off's colour, {b} in Deutan's");
|
||||
}
|
||||
|
||||
/// A mode's table reaches the CPU picture: the sketch states and axes it changes, and nothing
|
||||
/// else (the light theme in Off is the engine's own colours).
|
||||
/// A mode's table reaches the headless picture's palette: the sketch states and axes it
|
||||
/// changes, and nothing else (the light theme in Off is the engine's own colours).
|
||||
#[test]
|
||||
fn cpu_picture_follows_the_colour_vision_mode() {
|
||||
fn picture_palette_follows_the_colour_vision_mode() {
|
||||
use crate::theme::ColorVision;
|
||||
let rgb = |c: Color32| Rgb(c.r(), c.g(), c.b());
|
||||
let off = scene_palette(false, ColorVision::Off);
|
||||
@@ -5505,6 +5419,7 @@ mod tests {
|
||||
|
||||
/// The mode is on from the next frame (theme, widgets, caches, `ui.render`), and Cancel puts
|
||||
/// it back.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn colour_vision_mode_reaches_the_frame_and_the_view_caches() {
|
||||
use crate::theme::ColorVision;
|
||||
@@ -5524,8 +5439,11 @@ mod tests {
|
||||
h.frame();
|
||||
assert_eq!((h.app.preferences.color_vision, crate::theme::vision()), (ColorVision::Off, ColorVision::Off));
|
||||
assert_eq!(keys(&h.app), before);
|
||||
// `ui.render` (the CPU picture) shows the mode too: a fixed sketch line in Deutan's value
|
||||
// (in the light theme, where the red-green table changes it; the render follows the theme).
|
||||
// `ui.render` shows the mode too: a fixed sketch line in Deutan's value (in the light
|
||||
// theme, where the red-green table changes it; the render follows the theme).
|
||||
if !crate::gpu::test_picture_renderer() {
|
||||
return;
|
||||
}
|
||||
h.call("ui.prefs", json!({"theme": "light"}));
|
||||
h.frame();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
|
||||
Reference in new issue
Block a user