The engine's headless scene takes a palette; ui.render and the CPU viewport build it from the window's theme tokens, so a dark window renders a dark picture. CLI snapshots, MCP screenshots and Home thumbnails keep the default colours.
This commit is contained in:
1 parent
1a456d2d1f
commit
8f48544021
5 files changed
+422
-35
No files matched your search
@@ -2858,13 +2858,18 @@ fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter,
|
||||
v.to_bits().hash(&mut h);
|
||||
}
|
||||
(rect.width() as u32, rect.height() as u32).hash(&mut h);
|
||||
app.ui.dark.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 scene =
|
||||
solidcraft_engine::view::scene_with_section_and_sketch_sampler(&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
|
||||
});
|
||||
let palette = scene_palette(app.ui.dark);
|
||||
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);
|
||||
@@ -2877,6 +2882,29 @@ fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter,
|
||||
}
|
||||
}
|
||||
|
||||
/// The colours of a CPU picture of the model (`ui.render` and the CPU viewport) in the dark or
|
||||
/// the light theme, from the tokens the window draws with, so the picture matches the window. The
|
||||
/// light theme's are the engine's own colours, so a light render is unchanged. The sketch states
|
||||
/// and axes the window draws in the engine's colours stay so.
|
||||
pub(crate) fn scene_palette(dark: bool) -> solidcraft_engine::view::Palette {
|
||||
let t = if dark { Tokens::dark() } else { Tokens::light() };
|
||||
let rgb = |c: Color32| Rgb(c.r(), c.g(), c.b());
|
||||
solidcraft_engine::view::Palette {
|
||||
background: (rgb(t.viewport_top), rgb(t.viewport_bottom)),
|
||||
grid: rgb(t.grid),
|
||||
grid_major: rgb(t.grid_major),
|
||||
edge: rgb(t.body_edge),
|
||||
body: rgb(t.body),
|
||||
body_selected: rgb(t.body_selected),
|
||||
sketch: rgb(t.sketch),
|
||||
sketch_fixed: rgb(t.sketch_fixed),
|
||||
sketch_done: rgb(t.sketch_fixed),
|
||||
sketch_locked: rgb(t.sketch_locked),
|
||||
cap: rgb(t.section_cap),
|
||||
..solidcraft_engine::view::Palette::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_layout(app: &mut SolveApp, scale: f32) {
|
||||
if let Some(rect) = app.viewport.rect {
|
||||
let docks: Vec<_> =
|
||||
@@ -3478,6 +3506,76 @@ 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).
|
||||
/// The light theme's tokens are the engine's own colours, so a light picture is unchanged; in
|
||||
/// the dark theme each part takes the token the window draws it with. Sketch states follow
|
||||
/// what the window does with them (`sketch_color`), so the two can't drift apart.
|
||||
#[test]
|
||||
fn cpu_pictures_are_drawn_in_the_windows_theme() {
|
||||
use solidcraft_engine::view::Palette;
|
||||
assert_eq!(scene_palette(false), Palette::DEFAULT, "the light theme is the engine's colours");
|
||||
let rgb = |c: Color32| Rgb(c.r(), c.g(), c.b());
|
||||
let (p, t) = (scene_palette(true), Tokens::dark());
|
||||
assert_eq!(p.background, (rgb(t.viewport_top), rgb(t.viewport_bottom)));
|
||||
assert_eq!(
|
||||
[p.grid, p.grid_major, p.edge, p.body, p.body_selected, p.cap],
|
||||
[t.grid, t.grid_major, t.body_edge, t.body, t.body_selected, t.section_cap].map(rgb)
|
||||
);
|
||||
assert_ne!(p.background, Palette::DEFAULT.background);
|
||||
for dark in [false, true] {
|
||||
let (p, t) = (scene_palette(dark), if dark { Tokens::dark() } else { Tokens::light() });
|
||||
for (tag, cpu) in [
|
||||
(colors::SKETCH, p.sketch),
|
||||
(colors::SKETCH_FIXED, p.sketch_fixed),
|
||||
(colors::SKETCH_DONE, p.sketch_done),
|
||||
(colors::SKETCH_LOCKED, p.sketch_locked),
|
||||
(colors::SKETCH_PROJECTED, p.sketch_projected),
|
||||
(colors::SKETCH_LOST, p.sketch_lost),
|
||||
(colors::SKETCH_CONSTRUCTION, p.sketch_construction),
|
||||
(colors::SKETCH_CENTERLINE, p.sketch_centerline),
|
||||
] {
|
||||
let [r, g, b, _] = sketch_color(&t, tag);
|
||||
assert_eq!(cpu, Rgb(r, g, b), "dark {dark}: the window draws {tag:?} in another colour");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The CPU viewport follows a theme change at once: the theme is part of its picture's key.
|
||||
#[test]
|
||||
fn cpu_viewport_redraws_in_the_new_theme() {
|
||||
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
|
||||
};
|
||||
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 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);
|
||||
assert!(near, "dark {dark}: top row {px:?}, the window's background {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_viewport_uses_section_preview_and_sketch_slice() {
|
||||
fn pixels(h: &mut crate::scenario::Harness, cam: Camera) -> Vec<Color32> {
|
||||
|
||||
Reference in new issue
Block a user