Viewport: a legend for the Draft, Accessibility and Curvature Map analyses (#1299)

While Draft, Accessibility or Curvature Map analysis is on, a legend at the bottom left of the viewport says what each colour means and which values it stands for, with the theme's analysis colours and stripes. Labels translated in en/de/ja/ko/zh. Fixes #973.
This commit is contained in:
DuBearte authored and GitHub committed 2026-10-15 02:13:44 -04:00
1 parent 873a65fd4d
commit 6ef0b22118
6 files changed
+113

No files matched your search

+4
View File
@@ -293,6 +293,10 @@
"ui.quantity": "Anzahl",
"ui.quit": "Beenden",
"ui.radius": "Radius",
"ui.pull_direction": "Entformungsrichtung",
"ui.tool_direction": "Werkzeugrichtung",
"ui.reachable": "Erreichbar",
"ui.blocked": "Verdeckt",
"ui.radius_type": "Radius-Typ",
"ui.rectangle": "Rechteck",
"ui.rectangular_pattern": "Rechteckmuster",
+4
View File
@@ -592,6 +592,10 @@
"ui.quit": "Quit",
"ui.radial_context_menu": "Radial context menu",
"ui.radius": "Radius",
"ui.pull_direction": "Pull direction",
"ui.tool_direction": "Tool direction",
"ui.reachable": "Reachable",
"ui.blocked": "Blocked",
"ui.radius_type": "Radius Type",
"ui.rectangle": "Rectangle",
"ui.rectangular_pattern": "Rectangular Pattern",
+4
View File
@@ -592,6 +592,10 @@
"ui.quit": "終了",
"ui.radial_context_menu": "パイ メニュー",
"ui.radius": "半径",
"ui.pull_direction": "抜き方向",
"ui.tool_direction": "工具方向",
"ui.reachable": "到達可能",
"ui.blocked": "到達不可",
"ui.radius_type": "半径タイプ",
"ui.rectangle": "長方形",
"ui.rectangular_pattern": "矩形パターン",
+4
View File
@@ -293,6 +293,10 @@
"ui.quantity": "수량",
"ui.quit": "종료",
"ui.radius": "반지름",
"ui.pull_direction": "빼기 방향",
"ui.tool_direction": "공구 방향",
"ui.reachable": "도달 가능",
"ui.blocked": "막힘",
"ui.radius_type": "반지름 유형",
"ui.rectangle": "직사각형",
"ui.rectangular_pattern": "직사각형 패턴",
+4
View File
@@ -590,6 +590,10 @@
"ui.quit": "退出",
"ui.radial_context_menu": "环形上下文菜单",
"ui.radius": "半径",
"ui.pull_direction": "拔模方向",
"ui.tool_direction": "刀具方向",
"ui.reachable": "可到达",
"ui.blocked": "被遮挡",
"ui.radius_type": "半径类型",
"ui.rectangle": "矩形",
"ui.rectangular_pattern": "矩形阵列",
+93
View File
@@ -1371,6 +1371,76 @@ fn analysis_uniform(app: &SolveApp) -> [f32; 8] {
}
}
/// While Draft, Accessibility or Curvature Map analysis is on, what each of its colours means and
/// the values it stands for, at the bottom left of the viewport (#973). The colours alone don't
/// say, and with colour vision loss they can be hard to tell apart.
fn analysis_legend(app: &SolveApp, painter: &egui::Painter, rect: Rect) {
use solidcraft_engine::SurfaceAnalysis as A;
let t = Tokens::get();
let tr = crate::i18n::tr_label;
let col = |c: [f32; 3]| {
let u = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
Color32::from_rgb(u(c[0]), u(c[1]), u(c[2]))
};
let dir = |v: Vec3| format!("({:.2}, {:.2}, {:.2})", v.x, v.y, v.z);
let a = &t.analysis;
// (title, the direction it measures along, rows of colour, text, hatched).
let (title, along, rows): (&str, Option<String>, Vec<(Color32, String, bool)>) = match app.session.analysis {
Some(A::Draft { pull, angle }) => (
tr("Draft Analysis"),
Some(format!("{} {}", tr("Pull direction"), dir(pull))),
vec![
(col(a.draft[0]), format!("≥ {angle:.1}°"), false),
(col(a.draft[1]), format!("−{angle:.1}° … {angle:.1}°"), false),
(col(a.draft[2]), format!("≤ −{angle:.1}°"), a.hatch),
],
),
Some(A::Access { dir: d }) => (
tr("Accessibility Analysis"),
Some(format!("{} {}", tr("Tool direction"), dir(d))),
vec![(col(a.access[0]), tr("Reachable").to_string(), false), (col(a.access[1]), tr("Blocked").to_string(), a.hatch)],
),
// The shader's scale reaches its middle at twice the analysis radius.
Some(A::Curvature { radius }) => (
tr("Curvature Map Analysis"),
None,
vec![
(col(a.curvature[0]), tr("Flat").to_string(), false),
(col(a.curvature[1]), format!("{} {}", tr("Radius"), crate::prefs::show_mm(app, 2.0 * radius, 1)), false),
(col(a.curvature[2]), format!("{} ≤ {}", tr("Radius"), crate::prefs::show_mm(app, radius, 1)), false),
],
),
_ => return,
};
let (pad, row) = (8.0, 18.0);
let h = pad * 2.0 + 18.0 + along.as_ref().map_or(0.0, |_| row) + row * rows.len() as f32;
let r = Rect::from_min_size(pos2(rect.left() + 12.0, rect.bottom() - 12.0 - h), vec2(230.0, h));
painter.rect(r, 6.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
crate::scenario::publish_handle("analysis:legend", r.center());
let mut y = r.top() + pad;
painter.text(pos2(r.left() + pad, y), Align2::LEFT_TOP, title, FontId::proportional(13.0), t.text);
y += 18.0;
if let Some(along) = along {
painter.text(pos2(r.left() + pad, y), Align2::LEFT_TOP, along, FontId::proportional(11.5), t.text_dim);
y += row;
}
for (c, text, hatched) in rows {
let sw = Rect::from_min_size(pos2(r.left() + pad, y + 2.0), vec2(12.0, 12.0));
painter.rect_filled(sw, 2.0, c);
if hatched {
// The stripes the viewport adds to these faces (colour vision modes).
for k in [3.0, 7.0, 11.0] {
painter.line_segment(
[pos2(sw.left() + k - 3.0, sw.bottom()), pos2(sw.left() + k + 1.0, sw.top())],
Stroke::new(1.2, Color32::from_black_alpha(110)),
);
}
}
painter.text(pos2(sw.right() + 8.0, y), Align2::LEFT_TOP, text, FontId::proportional(12.0), t.text);
y += row;
}
}
fn section_clip(app: &SolveApp) -> [f32; 4] {
match section_plane(app) {
Some((o, n)) => [n.x as f32, n.y as f32, n.z as f32, n.dot(o) as f32],
@@ -2382,6 +2452,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
} else {
cpu_render(app, ui.ctx(), &painter, rect, &proj);
}
analysis_legend(app, &painter, rect);
// ---- interaction ----
let add = mods.shift || mods.command || mods.ctrl;
@@ -4602,6 +4673,28 @@ mod tests {
assert_eq!(d.items().len(), 1, "the other half toggles the same wall");
}
#[test]
fn colour_analyses_show_a_legend() {
// #973: Draft, Accessibility and Curvature Map say what their colours mean; Zebra and no
// analysis draw no legend.
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.app.run("solid.box", json!({"length":20,"width":20,"height":20})).unwrap();
let legend = |h: &mut crate::scenario::Harness| {
h.frames(2);
crate::scenario::handle_at("analysis:legend").is_some()
};
assert!(!legend(&mut h));
for (cmd, shown) in [("inspect.draft", true), ("inspect.accessibility", true), ("inspect.curvature_map", true), ("inspect.zebra", false)] {
h.app.session.analysis = None;
h.app.run(cmd, json!({})).unwrap();
assert!(h.app.session.analysis.is_some(), "{cmd}");
assert_eq!(legend(&mut h), shown, "{cmd}");
}
h.app.session.analysis = None;
assert!(!legend(&mut h));
}
#[test]
fn a_section_leaves_the_origin_planes_and_axes_whole() {
// #1229: the origin planes and axes are reference geometry; the section plane cuts the