194 lines
8.9 KiB
Rust
194 lines
8.9 KiB
Rust
//! A scene's colours come from its [`Palette`] (#974): the app draws `ui.render` in its theme,
|
|
//! while `solidcraft-cli snapshot`, MCP screenshots and thumbnails keep [`Palette::DEFAULT`].
|
|
|
|
use super::*;
|
|
use serde_json::json;
|
|
use solidcraft_geom::Vec2;
|
|
use solidcraft_sketch::{LinkGeom, LinkKind, LinkSource};
|
|
|
|
/// No colour here is one of [`colors`] or an appearance below, and each field has its own.
|
|
const MARK: Palette = Palette {
|
|
background: (Rgb(1, 2, 3), Rgb(4, 5, 6)),
|
|
grid: Rgb(7, 8, 9),
|
|
grid_major: Rgb(10, 11, 12),
|
|
edge: Rgb(13, 14, 15),
|
|
body: Rgb(16, 17, 18),
|
|
body_selected: Rgb(19, 20, 21),
|
|
sketch: Rgb(22, 23, 24),
|
|
sketch_fixed: Rgb(25, 26, 27),
|
|
sketch_done: Rgb(28, 29, 30),
|
|
sketch_locked: Rgb(31, 32, 33),
|
|
sketch_projected: Rgb(34, 35, 36),
|
|
sketch_lost: Rgb(37, 38, 39),
|
|
sketch_construction: Rgb(40, 41, 42),
|
|
sketch_centerline: Rgb(43, 44, 45),
|
|
axes: [Rgb(46, 47, 48), Rgb(49, 50, 51), Rgb(52, 53, 54)],
|
|
cap: Rgb(55, 56, 57),
|
|
analysis: solidcraft_render::AnalysisColors {
|
|
draft: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]],
|
|
access: [[0.11, 0.12, 0.13], [0.14, 0.15, 0.16]],
|
|
curvature: [[0.17, 0.18, 0.19], [0.21, 0.22, 0.23], [0.24, 0.25, 0.26]],
|
|
hatch: true,
|
|
},
|
|
};
|
|
|
|
/// Each curve's lines at x = its index, so a line tells which curve it draws.
|
|
fn by_index(_: &Sketch, i: usize, _: &Plane) -> Vec<Vec<Vec3>> {
|
|
vec![vec![Vec3::new(i as f64, 0., 0.), Vec3::new(i as f64, 1., 0.)]]
|
|
}
|
|
|
|
#[test]
|
|
fn the_default_palette_is_the_colour_table() {
|
|
let p = Palette::default();
|
|
assert_eq!(p, Palette::DEFAULT);
|
|
assert_eq!(p.background, (colors::BG_TOP, colors::BG_BOTTOM));
|
|
assert_eq!(
|
|
(p.grid, p.grid_major, p.edge, p.body, p.body_selected),
|
|
(colors::GRID, colors::GRID_MAJOR, colors::EDGE, colors::BODY, colors::BODY_SELECTED)
|
|
);
|
|
assert_eq!(
|
|
(p.sketch, p.sketch_fixed, p.sketch_done, p.sketch_locked),
|
|
(colors::SKETCH, colors::SKETCH_FIXED, colors::SKETCH_DONE, colors::SKETCH_LOCKED)
|
|
);
|
|
assert_eq!(
|
|
(p.sketch_projected, p.sketch_lost, p.sketch_construction, p.sketch_centerline),
|
|
(colors::SKETCH_PROJECTED, colors::SKETCH_LOST, colors::SKETCH_CONSTRUCTION, colors::SKETCH_CENTERLINE)
|
|
);
|
|
assert_eq!((p.axes, p.cap), ([colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z], colors::SECTION_CAP));
|
|
assert_eq!(p.analysis, solidcraft_render::AnalysisColors::DEFAULT);
|
|
}
|
|
|
|
#[test]
|
|
fn each_sketch_state_takes_its_palette_colour() {
|
|
let line = |sk: &mut Sketch, y: f64| sk.add_line(Vec2::new(0., y), Vec2::new(2., y), None, None, None).unwrap();
|
|
let mut sk = Sketch::new();
|
|
let ordinary = line(&mut sk, 0.);
|
|
let fixed = line(&mut sk, 1.);
|
|
sk.curves[fixed].fixed = true;
|
|
let centerline = line(&mut sk, 2.);
|
|
sk.curves[centerline].centerline = true;
|
|
let construction = line(&mut sk, 3.);
|
|
sk.curves[construction].construction = true;
|
|
let determined = line(&mut sk, 4.);
|
|
sk.add_link(LinkKind::Project, LinkSource::Origin, &[LinkGeom::Line(Vec2::new(0., 5.), Vec2::new(2., 5.))]).unwrap();
|
|
let projected = sk.curves.len() - 1;
|
|
let lost_link = sk.add_link(LinkKind::Project, LinkSource::Origin, &[LinkGeom::Line(Vec2::new(0., 6.), Vec2::new(2., 6.))]).unwrap();
|
|
sk.set_link_lost(&lost_link, true);
|
|
let lost = sk.curves.len() - 1;
|
|
let text = LinkSource::Text {
|
|
text: "A".into(),
|
|
at: Vec2::ZERO,
|
|
height: 2.,
|
|
angle: 0.,
|
|
font: None,
|
|
style: Default::default(),
|
|
exprs: Default::default(),
|
|
};
|
|
sk.add_link(LinkKind::Text, text, &[LinkGeom::Line(Vec2::new(0., 7.), Vec2::new(2., 7.))]).unwrap();
|
|
let text = sk.curves.len() - 1;
|
|
let mut flags = vec![false; sk.curves.len()];
|
|
flags[determined] = true;
|
|
let colour_of = |active: bool, i: usize| {
|
|
let lines = sketch_lines_in(&sk, &Plane::XY, active, &flags, by_index, &MARK);
|
|
let mut of_curve = lines.iter().filter(|(pts, _, _)| pts.first().is_some_and(|p| p.x == i as f64)).map(|(_, c, _)| *c);
|
|
let c = of_curve.next();
|
|
assert!(of_curve.all(|other| Some(other) == c), "curve {i} drawn in two colours");
|
|
c
|
|
};
|
|
for (i, want) in [
|
|
(ordinary, MARK.sketch),
|
|
(determined, MARK.sketch_fixed),
|
|
(fixed, MARK.sketch_locked),
|
|
(centerline, MARK.sketch_centerline),
|
|
(construction, MARK.sketch_construction),
|
|
(projected, MARK.sketch_projected),
|
|
(lost, MARK.sketch_lost),
|
|
(text, MARK.sketch),
|
|
] {
|
|
assert_eq!(colour_of(true, i), Some(want), "curve {i} in the active sketch");
|
|
}
|
|
for i in [ordinary, determined, text] {
|
|
assert_eq!(colour_of(false, i), Some(MARK.sketch_done), "curve {i} in a finished sketch");
|
|
}
|
|
// The public function is the default palette: the colours the GPU viewport reads as tags.
|
|
assert_eq!(
|
|
sketch_lines_in(&sk, &Plane::XY, true, &flags, by_index, &Palette::DEFAULT),
|
|
sketch_lines_with_sampler(&sk, &Plane::XY, true, &flags, by_index)
|
|
);
|
|
}
|
|
|
|
/// Every colour of a scene comes from its palette, except what the design says: a body's
|
|
/// appearance (even one equal to the default grey) and a face's own colour.
|
|
#[test]
|
|
fn every_scene_colour_comes_from_its_palette_and_appearances_stay() {
|
|
let mut s = Session::default();
|
|
let new_body = |s: &mut Session, name: &str, x: f64| {
|
|
s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0], "operation": "new_body", "body_name": name}))
|
|
.unwrap();
|
|
};
|
|
new_body(&mut s, "Grey", 0.);
|
|
new_body(&mut s, "Picked", 20.);
|
|
new_body(&mut s, "Plain", 40.);
|
|
// Exactly the default body colour, as an appearance: the design's, so it stays.
|
|
s.execute("appearance.assign", &json!({"bodies": ["Grey"], "color": "#b0bac6"})).unwrap();
|
|
s.execute("appearance.assign", &json!({"body": "Plain", "faces": [[45, 5, 10]], "color": "#00ff00"})).unwrap();
|
|
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
|
s.execute("sketch.line", &json!({"points": [[0, -20], [30, -20]]})).unwrap();
|
|
s.execute("sketch.finish", &json!({})).unwrap();
|
|
s.selection = vec![Sel::Body { name: "Picked".into() }];
|
|
s.section = Some((Vec3::new(0., 0., 5.), Vec3::Z));
|
|
let cam = home_camera(&s);
|
|
let names: Vec<String> = s.world_state().bodies.iter().filter(|b| s.body_visible(&b.name)).map(|b| b.name.clone()).collect();
|
|
let mesh = |sc: &Scene, name: &str| {
|
|
names.iter().position(|n| n == name).and_then(|i| sc.meshes.get(i)).map(|m| (m.color, m.face_colors.clone())).unwrap()
|
|
};
|
|
|
|
let marked = scene_with_palette(&s, &cam, s.section, world_curve_parts, &MARK);
|
|
assert_eq!((marked.background, marked.cap), (Some(MARK.background), Some(MARK.cap)));
|
|
let allowed = [
|
|
MARK.grid,
|
|
MARK.grid_major,
|
|
MARK.edge,
|
|
MARK.axes[0],
|
|
MARK.axes[1],
|
|
MARK.axes[2],
|
|
MARK.sketch,
|
|
MARK.sketch_fixed,
|
|
MARK.sketch_done,
|
|
MARK.sketch_locked,
|
|
MARK.sketch_projected,
|
|
MARK.sketch_lost,
|
|
MARK.sketch_construction,
|
|
MARK.sketch_centerline,
|
|
];
|
|
let all_lines = || marked.grid.iter().chain(&marked.lines);
|
|
let stray: Vec<Rgb> = all_lines().map(|l| l.color).filter(|c| !allowed.contains(c)).collect();
|
|
assert!(stray.is_empty(), "line colours not from the palette: {stray:?}");
|
|
for want in [MARK.grid, MARK.grid_major, MARK.edge, MARK.axes[0], MARK.axes[1], MARK.axes[2], MARK.sketch_done] {
|
|
assert!(all_lines().any(|l| l.color == want), "no line in {want:?}");
|
|
}
|
|
assert!(marked.grid.iter().all(|l| [MARK.grid, MARK.grid_major].contains(&l.color)), "only the grid is drawn under the model");
|
|
assert_eq!(mesh(&marked, "Grey").0, colors::BODY, "an appearance equal to the default grey is the design's");
|
|
assert_eq!(mesh(&marked, "Picked").0, MARK.body_selected);
|
|
let (plain, faces) = mesh(&marked, "Plain");
|
|
assert_eq!(plain, MARK.body);
|
|
assert!(!faces.is_empty() && faces.iter().all(|(_, c)| *c == Rgb(0, 255, 0)), "face colours are the design's: {faces:?}");
|
|
|
|
// The default palette is the scene everyone else gets, line for line.
|
|
let default = scene_with_palette(&s, &cam, s.section, world_curve_parts, &Palette::DEFAULT);
|
|
let plain_scene = scene(&s, &cam);
|
|
assert_eq!(
|
|
(default.background, default.cap, default.clip, default.radius),
|
|
(plain_scene.background, plain_scene.cap, plain_scene.clip, plain_scene.radius)
|
|
);
|
|
assert_eq!((default.lines.len(), default.grid.len()), (plain_scene.lines.len(), plain_scene.grid.len()));
|
|
for (a, b) in default.lines.iter().zip(&plain_scene.lines).chain(default.grid.iter().zip(&plain_scene.grid)) {
|
|
assert_eq!((&a.points, a.color, a.width, a.on_top), (&b.points, b.color, b.width, b.on_top));
|
|
}
|
|
for name in &names {
|
|
assert_eq!(mesh(&default, name), mesh(&plain_scene, name), "{name}");
|
|
}
|
|
assert_eq!(mesh(&default, "Plain").0, colors::BODY);
|
|
assert_eq!(mesh(&default, "Picked").0, colors::BODY_SELECTED);
|
|
}
|