From 6ef0b221183b83b919a16c6cc80666888d867e8a Mon Sep 17 00:00:00 2001 From: DuBearte <130190089+DuBearte@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:13:44 -0400 Subject: [PATCH] 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. --- assets/locales/de.json | 4 ++ assets/locales/en.json | 4 ++ assets/locales/ja.json | 4 ++ assets/locales/ko.json | 4 ++ assets/locales/zh.json | 4 ++ crates/ui-egui/src/viewport.rs | 93 ++++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+) diff --git a/assets/locales/de.json b/assets/locales/de.json index 5e2ec39d..946872ce 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -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", diff --git a/assets/locales/en.json b/assets/locales/en.json index ae2dba76..c06fe8c4 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -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", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index 3953cb1b..db1df53b 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -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": "矩形パターン", diff --git a/assets/locales/ko.json b/assets/locales/ko.json index e9551e55..b5e11d4c 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -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": "직사각형 패턴", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index c1ba0886..c7c0ff28 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -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": "矩形阵列", diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index eb612278..3f811d7b 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -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, 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