Files
solidcraft-upstream/crates/ui-egui/src/colour_tokens_tests.rs
T

212 lines
9.9 KiB
Rust

//! Every colour that carries a meaning comes from `theme::Tokens` (#778): the engine's sketch and
//! axis tags, the surface analysis colours and every colour an icon draws. These tests hold that
//! the tokens equal what was drawn before, and that a new colour cannot skip them.
use egui::Color32;
use solidcraft_engine::render::Rgb;
use solidcraft_engine::view::colors;
use crate::theme::Tokens;
fn rgb(c: Rgb) -> Color32 {
Color32::from_rgb(c.0, c.1, c.2)
}
/// The new tokens hold exactly the values that were drawn in place before they were tokens, so
/// with no colour vision mode on, nothing on screen changes.
#[test]
fn moved_colours_keep_the_values_they_were_drawn_with() {
for t in [Tokens::light(), Tokens::dark()] {
assert_eq!(t.sketch_projected, rgb(colors::SKETCH_PROJECTED));
assert_eq!(t.sketch_lost, rgb(colors::SKETCH_LOST));
assert_eq!(t.sketch_construction, rgb(colors::SKETCH_CONSTRUCTION));
assert_eq!(t.sketch_centerline, rgb(colors::SKETCH_CENTERLINE));
assert_eq!(t.triad, [rgb(colors::AXIS_X), rgb(colors::AXIS_Y), rgb(colors::AXIS_Z)]);
assert_eq!(t.gizmo, [Color32::from_rgb(225, 70, 60), Color32::from_rgb(80, 190, 80), Color32::from_rgb(70, 130, 240)]);
assert_eq!(t.interference, Color32::from_rgb(235, 45, 40));
// The shader's former literals, bit for bit.
assert_eq!(t.analysis.draft, [[0.20, 0.75, 0.30], [0.95, 0.80, 0.15], [0.85, 0.22, 0.20]]);
assert_eq!(t.analysis.access, [[0.20, 0.75, 0.30], [0.85, 0.22, 0.20]]);
assert_eq!(t.analysis.curvature, [[0.15, 0.35, 0.95], [0.10, 0.80, 0.25], [0.90, 0.15, 0.10]]);
assert!(!t.analysis.hatch);
assert_eq!(t.app_bar_icon, [Color32::from_rgb(225, 230, 238), Color32::from_rgb(120, 140, 170), Color32::from_rgb(120, 190, 255)]);
assert_eq!(t.logo, [Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197), Color32::WHITE]);
assert_eq!(t.icons.glint, Color32::from_white_alpha(160));
assert_eq!(t.icons.marker_halo, Color32::from_black_alpha(150));
assert_eq!(t.icons.agent, Color32::from_rgb(168, 85, 247));
}
let (l, d) = (Tokens::light(), Tokens::dark());
assert_eq!((l.bend_up, l.bend_down), (Color32::from_rgb(30, 140, 50), Color32::from_rgb(200, 90, 20)));
assert_eq!((d.bend_up, d.bend_down), (Color32::from_rgb(110, 200, 120), Color32::from_rgb(240, 150, 80)));
}
/// The analysis colours reach the shader in the order its `ana_c` array reads them.
#[test]
fn analysis_colours_reach_the_shader_in_its_order() {
let a = Tokens::light().analysis;
let v = crate::gpu::analysis_colors(&a);
let at = |i: usize| [v[4 * i], v[4 * i + 1], v[4 * i + 2]];
assert_eq!([at(0), at(1), at(2)], a.draft);
assert_eq!([at(3), at(4)], a.access);
assert_eq!([at(5), at(6), at(7)], a.curvature);
assert!((0..8).all(|i| v[4 * i + 3] == 1.0));
assert_eq!(v[32], 0.0, "no stripes without a colour vision mode");
let mut striped = a;
striped.hatch = true;
assert_eq!(crate::gpu::analysis_colors(&striped)[32], 1.0);
}
// Icons no longer take their colours from the caller or from `IconColors`: the SVG masters name
// palette roles (`assets/icons/ui/design/palette.toml`, light and dark, plus accents) that
// `icon_svg` resolves. `icon_svg`'s own tests hold that every master parses and that only the
// palette's colours exist; the colour vision modes do not reach them yet.
/// The most colour literals each UI file may hold, and why. None of them carries a meaning that a
/// colour vision mode must change. A file with more fails here: make the new colour a token in
/// `theme::Tokens` (and `assets/styles/ui/palettes.rs`) instead, or, if it carries no meaning,
/// raise the file's count here and give the reason. Fewer is always fine. The icon sources may
/// hold none at all.
const ALLOWED_LITERALS: &[(&str, usize, &str)] = &[
("navigation.rs", 1, "the navigation cube's face shading, made from a token"),
("canvas.rs", 3, "white rings around manipulator handles"),
("context_menu.rs", 1, "the radial menu's shadow"),
("dialogs_appearance.rs", 1, "a design appearance: the colour is the fact"),
("dialogs_save_mesh.rs", 1, "an installed slicer's own icon, drawn untinted"),
("dialogs_library.rs", 1, "a library appearance's swatch: the colour is the fact"),
("dialogs_assembly.rs", 1, "the interference fill, shaded from its token"),
("dialogs_motion.rs", 1, "a white outline"),
("documents.rs", 4, "white hover overlays on document tabs"),
("frame.rs", 1, "the window shadow"),
("gizmo.rs", 2, "the gizmo's neutral grey ball"),
("icons.rs", 1, "the white tint that carries an icon image's opacity"),
("home.rs", 4, "white ink on accent buttons, and thumbnail tints"),
("ref_images.rs", 1, "a reference image's opacity tint"),
("sketch_palette.rs", 1, "white text on the accent button"),
("sketch_text.rs", 1, "white text on the selection chip"),
("titlebar.rs", 4, "the window close button's red hover (the system's convention), white hovers and ink"),
("toolbar.rs", 3, "white hover overlays and the version text"),
];
const COLOUR_LITERALS: &[&str] = &[
"Color32::from_rgb(",
"Color32::from_rgba_unmultiplied(",
"Color32::from_rgba_unmultiplied_const(",
"Color32::from_rgba_premultiplied(",
"Color32::from_gray(",
"Color32::from_black_alpha(",
"Color32::from_white_alpha(",
"Color32::from_hex(",
"Color32::WHITE",
"Color32::BLACK",
"Color32::RED",
"Color32::GREEN",
"Color32::BLUE",
"Color32::YELLOW",
"Color32::GOLD",
"Color32::ORANGE",
"Color32::BROWN",
"Color32::KHAKI",
"Color32::GRAY",
"Color32::LIGHT_GRAY",
"Color32::DARK_GRAY",
"Color32::DARK_RED",
"Color32::LIGHT_RED",
"Color32::DARK_BLUE",
"Color32::LIGHT_BLUE",
"Color32::DARK_GREEN",
"Color32::LIGHT_GREEN",
"Color32::LIGHT_YELLOW",
];
#[test]
fn colours_outside_the_theme_are_only_the_listed_neutral_ones() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let dirs = [root.join("src"), root.join("../../assets/icons/ui/source")];
let mut found = Vec::new();
for dir in &dirs {
let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("{}: {e}", dir.display()));
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default().to_string();
if !name.ends_with(".rs") || name.ends_with("_tests.rs") || name == "theme.rs" {
continue;
}
let src = std::fs::read_to_string(&path).unwrap_or_default();
let code = src.split("#[cfg(test)]").next().unwrap_or(&src);
let n: usize = COLOUR_LITERALS.iter().map(|p| code.matches(p).count()).sum();
let allowed = ALLOWED_LITERALS.iter().find(|(f, _, _)| *f == name).map_or(0, |(_, n, _)| *n);
if n > allowed {
found.push(format!("{name}: {n} colour literal(s), at most {allowed} allowed"));
}
}
}
assert!(found.is_empty(), "colour literals outside theme::Tokens: {found:?}");
}
/// The analysis shader takes its colours from the uniform (and so from the tokens). Only the
/// zebra and environment map neutrals and direction vectors stay as literals.
#[test]
fn analysis_shader_colours_come_from_the_uniform() {
let src = include_str!("../../../assets/shaders/viewport.wgsl");
let start = src.find("fn analysis(").unwrap_or(0);
let end = src.find("fn stripe(").unwrap_or(src.len());
let body = &src[start..end];
let mut literals = Vec::new();
let mut rest = body;
while let Some(i) = rest.find("vec3<f32>(") {
let tail = &rest[i..];
let close = tail.find(')').unwrap_or(tail.len() - 1);
literals.push(&tail[..=close]);
rest = &tail[close..];
}
assert_eq!(
literals,
[
"vec3<f32>(0.0, 0.0, 1.0)",
"vec3<f32>(1.0, 0.0, 0.0)",
"vec3<f32>(c)",
"vec3<f32>(0.32, 0.34, 0.38)",
"vec3<f32>(0.80, 0.86, 0.95)",
"vec3<f32>(0.6)",
"vec3<f32>(0.9)",
"vec3<f32>(0.55, 0.56, 0.60)",
"vec3<f32>(0.20, 0.20, 0.22)",
"vec3<f32>(0.35)",
"vec3<f32>(1.0)",
],
"a new colour in the analysis shader must come from `ana_c` (theme::AnalysisColors)"
);
assert_eq!(body.matches("u.ana_c[").count(), 9, "every analysis colour reads `ana_c`");
}
/// Every colour the engine tags a scene line with is mapped to a token by the viewport, or carries
/// no meaning a colour vision mode must change. A new engine tag fails here until it is placed.
#[test]
fn every_engine_colour_tag_is_placed() {
const MAPPED: &[&str] = &[
"SKETCH",
"SKETCH_DONE",
"SKETCH_FIXED",
"SKETCH_LOCKED",
"SKETCH_PROJECTED",
"SKETCH_LOST",
"SKETCH_CONSTRUCTION",
"SKETCH_CENTERLINE",
"AXIS_X",
"AXIS_Y",
"AXIS_Z",
];
// Bodies, edges, profiles, the grid and the background: the GPU viewport draws these from
// their own tokens; the engine's values are only the headless snapshot's.
const NEUTRAL: &[&str] = &["BODY", "BODY_SELECTED", "EDGE", "PROFILE", "GRID", "GRID_MAJOR", "BG_TOP", "BG_BOTTOM", "SECTION_CAP"];
let src = include_str!("../../engine/src/view.rs");
let start = src.find("pub mod colors {").unwrap_or(0);
let block = &src[start..];
let block = &block[..block.find("\n}").unwrap_or(block.len())];
let mut names: Vec<&str> = block.lines().filter_map(|l| l.trim().strip_prefix("pub const ")).filter_map(|l| l.split(':').next()).collect();
names.sort_unstable();
let mut placed: Vec<&str> = MAPPED.iter().chain(NEUTRAL).copied().collect();
placed.sort_unstable();
assert_eq!(names, placed, "an engine colour tag must be mapped to a token in `viewport::sketch_color` or listed as neutral");
}