Show optional X/Y/Z distances in measurement overlays (#1422)

Measure can show the world-axis components alongside the total distance, with a Graphics preference to turn them off.

Co-authored-by: cronos <19868002+crono2250@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-15 19:09:26 -04:00
1 parent 3d914c9f15
commit 96d1ad0ab2
14 files changed
+272 -2

No files matched your search

+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "X/Y/Z-Maße anzeigen",
"settings.measure_axis_distances_help": "Bei geöffneter Messung die Abstände entlang der Weltachsen zusätzlich zum Gesamtabstand anzeigen.",
"Send your report on GitHub": "Bericht auf GitHub senden", "Send your report on GitHub": "Bericht auf GitHub senden",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Unten den GitHub-Entwurf öffnen und den kopierten Bericht aus der Zwischenablage einfügen.", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Unten den GitHub-Entwurf öffnen und den kopierten Bericht aus der Zwischenablage einfügen.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Unten den GitHub-Entwurf öffnen. Titel und Bericht werden vorausgefüllt.", "1. Open GitHub draft below. Your title and report will be filled in.": "1. Unten den GitHub-Entwurf öffnen. Titel und Bericht werden vorausgefüllt.",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "Show X/Y/Z distances",
"settings.measure_axis_distances_help": "Show world-axis component dimensions alongside the total distance while Measure is open.",
"Send your report on GitHub": "Send your report on GitHub", "Send your report on GitHub": "Send your report on GitHub",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Open GitHub draft below, then paste the report copied to your clipboard.", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Open GitHub draft below, then paste the report copied to your clipboard.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Open GitHub draft below. Your title and report will be filled in.", "1. Open GitHub draft below. Your title and report will be filled in.": "1. Open GitHub draft below. Your title and report will be filled in.",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "Afficher les cotes X/Y/Z",
"settings.measure_axis_distances_help": "Afficher les cotes suivant les axes du repère global en plus de la distance totale pendant la mesure.",
"Send your report on GitHub": "Envoyer votre rapport sur GitHub", "Send your report on GitHub": "Envoyer votre rapport sur GitHub",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Ouvrir le brouillon GitHub ci-dessous, puis coller le rapport copié dans le presse-papiers.", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Ouvrir le brouillon GitHub ci-dessous, puis coller le rapport copié dans le presse-papiers.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Ouvrir le brouillon GitHub ci-dessous. Votre titre et votre rapport y seront déjà remplis.", "1. Open GitHub draft below. Your title and report will be filled in.": "1. Ouvrir le brouillon GitHub ci-dessous. Votre titre et votre rapport y seront déjà remplis.",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "計測のX・Y・Z距離を表示",
"settings.measure_axis_distances_help": "計測ダイアログを開いている間、2点間距離に加えてワールドX・Y・Z軸方向の寸法線と距離を表示します。",
"Send your report on GitHub": "GitHubでレポートを送信", "Send your report on GitHub": "GitHubでレポートを送信",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 下のボタンでGitHubの下書きを開き、クリップボードにコピーされたレポートを貼り付けます。", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 下のボタンでGitHubの下書きを開き、クリップボードにコピーされたレポートを貼り付けます。",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. 下のボタンでGitHubの下書きを開きます。タイトルとレポートは入力済みです。", "1. Open GitHub draft below. Your title and report will be filled in.": "1. 下のボタンでGitHubの下書きを開きます。タイトルとレポートは入力済みです。",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "X/Y/Z 거리 표시",
"settings.measure_axis_distances_help": "측정 대화상자가 열려 있는 동안 전체 거리와 함께 월드 축 방향 치수와 거리를 표시합니다.",
"Send your report on GitHub": "GitHub에서 보고서 보내기", "Send your report on GitHub": "GitHub에서 보고서 보내기",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 아래에서 GitHub 초안을 열고 클립보드에 복사된 보고서를 붙여넣으세요.", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 아래에서 GitHub 초안을 열고 클립보드에 복사된 보고서를 붙여넣으세요.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. 아래에서 GitHub 초안을 여세요. 제목과 보고서가 자동으로 입력됩니다.", "1. Open GitHub draft below. Your title and report will be filled in.": "1. 아래에서 GitHub 초안을 여세요. 제목과 보고서가 자동으로 입력됩니다.",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "Расстояния X/Y/Z",
"settings.measure_axis_distances_help": "Показывать размеры вдоль мировых осей вместе с общим расстоянием, пока открыто окно измерения.",
"browser.body_name": "Тело{number}", "browser.body_name": "Тело{number}",
"browser.component_name": "Компонент{number}", "browser.component_name": "Компонент{number}",
"browser.parameters": "Параметры ({count})", "browser.parameters": "Параметры ({count})",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "Відстані X/Y/Z",
"settings.measure_axis_distances_help": "Показувати розміри вздовж світових осей разом із загальною відстанню, поки відкрите вікно вимірювання.",
"Send your report on GitHub": "Надіслати звіт на GitHub", "Send your report on GitHub": "Надіслати звіт на GitHub",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Відкрийте чернетку GitHub нижче, а потім вставте звіт, скопійований у буфер обміну.", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Відкрийте чернетку GitHub нижче, а потім вставте звіт, скопійований у буфер обміну.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Відкрийте чернетку GitHub нижче. Заголовок і звіт буде заповнено.", "1. Open GitHub draft below. Your title and report will be filled in.": "1. Відкрийте чернетку GitHub нижче. Заголовок і звіт буде заповнено.",
+2
View File
@@ -1,4 +1,6 @@
{ {
"settings.measure_axis_distances": "显示 X/Y/Z 距离",
"settings.measure_axis_distances_help": "测量对话框打开时,在总距离之外显示沿世界坐标轴方向的尺寸线和距离。",
"Send your report on GitHub": "在 GitHub 上发送报告", "Send your report on GitHub": "在 GitHub 上发送报告",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 点击下方按钮打开 GitHub 草稿,然后粘贴已复制到剪贴板的报告。", "1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 点击下方按钮打开 GitHub 草稿,然后粘贴已复制到剪贴板的报告。",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. 点击下方按钮打开 GitHub 草稿。标题和报告将自动填入。", "1. Open GitHub draft below. Your title and report will be filled in.": "1. 点击下方按钮打开 GitHub 草稿。标题和报告将自动填入。",
+76
View File
@@ -769,7 +769,13 @@ fn radius_spline_handles(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Pain
fn measure_line(app: &SolveApp, painter: &egui::Painter, proj: &Proj, r: &serde_json::Value, show: Option<crate::dialogs::Shown>) { fn measure_line(app: &SolveApp, painter: &egui::Painter, proj: &Proj, r: &serde_json::Value, show: Option<crate::dialogs::Shown>) {
let t = Tokens::get(); let t = Tokens::get();
let Some((_, d, a, b)) = crate::dialogs::shown_distance(r, show) else { return }; let Some((_, d, a, b)) = crate::dialogs::shown_distance(r, show) else { return };
if !a.is_finite() || !b.is_finite() || !d.is_finite() {
return;
}
let (Some(sa), Some(sb)) = (proj.to_screen(a), proj.to_screen(b)) else { return }; let (Some(sa), Some(sb)) = (proj.to_screen(a), proj.to_screen(b)) else { return };
if !sa.is_finite() || !sb.is_finite() {
return;
}
painter.line_segment([sa, sb], Stroke::new(2.0, t.manipulator)); painter.line_segment([sa, sb], Stroke::new(2.0, t.manipulator));
for s in [sa, sb] { for s in [sa, sb] {
painter.circle(s, 3.5, t.manipulator, Stroke::new(1.0, Color32::WHITE)); painter.circle(s, 3.5, t.manipulator, Stroke::new(1.0, Color32::WHITE));
@@ -777,10 +783,80 @@ fn measure_line(app: &SolveApp, painter: &egui::Painter, proj: &Proj, r: &serde_
let mid = sa + (sb - sa) * 0.5 + vec2(10.0, -14.0); let mid = sa + (sb - sa) * 0.5 + vec2(10.0, -14.0);
let galley = painter.layout_no_wrap(crate::prefs::show_mm(app, d, 1), egui::FontId::proportional(12.5), t.text); let galley = painter.layout_no_wrap(crate::prefs::show_mm(app, d, 1), egui::FontId::proportional(12.5), t.text);
let rect = egui::Rect::from_min_size(mid, galley.size() + vec2(10.0, 6.0)); let rect = egui::Rect::from_min_size(mid, galley.size() + vec2(10.0, 6.0));
crate::scenario::publish_handle("measure:label", rect.center());
crate::scenario::publish_rect("measure:label", rect);
if app.preferences.measure_axis_distances {
measure_components(app, painter, proj, a, b, rect);
}
painter.rect(rect, 3.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); painter.rect(rect, 3.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
painter.galley(rect.min + vec2(5.0, 3.0), galley, t.text); painter.galley(rect.min + vec2(5.0, 3.0), galley, t.text);
} }
/// A world-axis-aligned chain between the same two measured points. Zero-length and
/// view-aligned legs still get a label; labels move apart when their projections coincide.
fn measure_components(app: &SolveApp, painter: &egui::Painter, proj: &Proj, a: Vec3, b: Vec3, total: egui::Rect) {
let t = Tokens::get();
let x = Vec3::new(b.x, a.y, a.z);
let y = Vec3::new(b.x, b.y, a.z);
let mut occupied = vec![total];
for (axis, start, end, color) in [("x", a, x, t.axis_x), ("y", x, y, t.axis_y), ("z", y, b, t.axis_z)] {
let color = color.lerp_to_gamma(t.text, 0.25);
let distance = start.dist(end);
if !distance.is_finite() {
continue;
}
let screen =
proj.to_screen(start).zip(proj.to_screen(end)).filter(|(sa, sb)| sa.is_finite() && sb.is_finite() && (*sb - *sa).length_sq().is_finite());
let (at, offset) = if let Some((sa, sb)) = screen {
let leg = sb - sa;
if leg.length_sq() > 1.0 {
painter.add(egui::Shape::dashed_line(&[sa, sb], Stroke::new(1.5, color), 5.0, 3.0));
let normal = vec2(-leg.y, leg.x).normalized();
for p in [sa, sb] {
painter.line_segment([p - normal * 3.0, p + normal * 3.0], Stroke::new(1.0, color));
}
(sa + leg * 0.5, normal * 20.0)
} else {
(sa, vec2(20.0, 20.0))
}
} else {
(total.center(), vec2(20.0, 20.0))
};
let text = format!("{}: {}", axis.to_uppercase(), crate::prefs::show_mm(app, distance, 1));
let galley = painter.layout_no_wrap(text, egui::FontId::proportional(12.5), t.text);
let size = galley.size() + vec2(10.0, 6.0);
let bounds = proj.rect.shrink(4.0);
let clamp = |rect: egui::Rect| {
egui::Rect::from_min_size(
egui::pos2(
rect.left().clamp(bounds.left(), (bounds.right() - size.x).max(bounds.left())),
rect.top().clamp(bounds.top(), (bounds.bottom() - size.y).max(bounds.top())),
),
size,
)
};
let preferred = clamp(egui::Rect::from_center_size(at + offset, size));
let mut rect = preferred;
// Four labels at most. Search both sides of the anchor, including at viewport edges.
'place: for row in 0..12 {
for side in [1.0, -1.0] {
let candidate = clamp(preferred.translate(vec2(0.0, side * row as f32 * (size.y + 4.0))));
if occupied.iter().all(|other| !other.expand(3.0).intersects(candidate)) {
rect = candidate;
break 'place;
}
}
}
painter.line_segment([at, rect.center()], Stroke::new(0.8, color));
painter.rect(rect, 3.0, t.overlay, Stroke::new(1.0, color), egui::StrokeKind::Inside);
painter.galley(rect.min + vec2(5.0, 3.0), galley, t.text);
let handle = format!("measure:label:{axis}");
crate::scenario::publish_handle(&handle, rect.center());
crate::scenario::publish_rect(&handle, rect);
occupied.push(rect);
}
}
/// The taper handle: a line from `foot` along the extrude (`span`) leaning by `angle` towards /// The taper handle: a line from `foot` along the extrude (`span`) leaning by `angle` towards
/// `e`, with a ring at its end; dragging the ring sideways sets the angle (1° steps, Alt/Ctrl: /// `e`, with a ring at its end; dragging the ring sideways sets the angle (1° steps, Alt/Ctrl:
/// free). /// free).
+2
View File
@@ -63,6 +63,8 @@ pub mod issue_report;
pub mod keymap; pub mod keymap;
mod material_card_review; mod material_card_review;
#[cfg(test)] #[cfg(test)]
mod measurement_ui_tests;
#[cfg(test)]
mod menu_tests; mod menu_tests;
#[cfg(test)] #[cfg(test)]
mod surface_clamp_tests; mod surface_clamp_tests;
+153
View File
@@ -0,0 +1,153 @@
//! World-axis component dimensions use the same endpoints as the displayed measurement.
use crate::dialogs::Kind;
use crate::scenario::Harness;
use serde_json::{Value, json};
use solidcraft_engine::Sel;
fn result(h: &Harness) -> &Value {
match &h.app.dialog.as_ref().unwrap().kind {
Kind::Measure { result: Some(r), .. } => r,
_ => panic!("expected a measurement result"),
}
}
fn vertex_measurement(a: [f64; 3], b: [f64; 3]) -> Harness {
let mut h = Harness::new();
h.app.home.open = false;
h.app.ui.show_origin = false;
h.app.ui.show_browser = false;
h.app.run("solid.box", json!({"length":3,"width":4,"height":12,"corner":[-3,0,-12],"body_name":"Box"})).unwrap();
h.app.session.selection = [a, b].into_iter().map(|p| Sel::Vertex { body: "Box".into(), point: p.into() }).collect();
h.app.start("inspect.measure");
h.call("ui.view", json!({"view":"fit"}));
h
}
/// Inspect the actual painted text, including the viewport labels (which are not widgets).
fn painted_text(h: &mut Harness) -> Vec<String> {
// Continue the harness clock (a hair on) so egui's input history never sees time go backwards.
let time = h.ctx.input(|i| i.time) + 1e-4;
let raw = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0))),
time: Some(time),
..Default::default()
};
let mut output = h.ctx.run_ui(raw, |ui| h.app.ui(ui));
output.textures_delta.clear();
output.shapes.iter().filter_map(|s| if let egui::Shape::Text(t) = &s.shape { Some(t.galley.text().to_string()) } else { None }).collect()
}
fn component_rects() -> Vec<egui::Rect> {
["measure:label", "measure:label:x", "measure:label:y", "measure:label:z"].into_iter().map(|key| crate::scenario::rect_of(key).unwrap()).collect()
}
#[test]
fn measurement_axis_distances_follow_the_upstream_centre_min_and_max_choices() {
let mut h = Harness::new();
h.app.home.open = false;
h.app.ui.show_origin = false;
h.app.ui.show_browser = false;
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
let mut items = Vec::new();
for (center, radius) in [([0, 0], 3), ([30, 40], 5)] {
let made = h.app.run("sketch.circle.center", json!({"center":center,"radius":radius})).unwrap();
items.push(Sel::SketchCurve { id: made["curves"][0].as_str().unwrap().into() });
}
h.app.session.selection = items;
h.app.start("inspect.measure");
h.call("ui.view", json!({"view":"fit"}));
let measured = result(&h).clone();
assert!((measured["center_distance"].as_f64().unwrap() - 50.).abs() < 1e-8);
assert!(measured["distance_mm"].as_f64().unwrap() < 50. && measured["max"].as_f64().unwrap() > 50.);
for (label, distance, from, to) in [
("Centre to centre", "center_distance", "center_from", "center_to"),
("Min", "distance_mm", "from", "to"),
("Max", "max", "max_from", "max_to"),
] {
let [x, y] = h.widget(label).unwrap_or_else(|| panic!("missing upstream distance choice: {label}"));
h.call("ui.click", json!({"x":x,"y":y}));
let texts = painted_text(&mut h);
for (axis, index) in [("X", 0), ("Y", 1), ("Z", 2)] {
let delta = measured[to][index].as_f64().unwrap() - measured[from][index].as_f64().unwrap();
let expected = format!("{axis}: {}", crate::prefs::show_mm(&h.app, delta.abs(), 1));
assert!(texts.contains(&expected), "{label} must use its own endpoints: {texts:?}");
}
assert!(texts.contains(&crate::prefs::show_mm(&h.app, measured[distance].as_f64().unwrap(), 1)));
assert_eq!(*result(&h), measured, "changing the displayed distance must retain upstream measurement results");
}
}
#[test]
fn measurement_axis_distances_show_absolute_world_components_in_every_view() {
for reverse in [false, true] {
let (a, b) = ([0., 0., 0.], [-3., 4., -12.]);
let mut h = if reverse { vertex_measurement(b, a) } else { vertex_measurement(a, b) };
assert!((result(&h)["distance_mm"].as_f64().unwrap() - 13.).abs() < 1e-8);
for view in ["iso", "top", "front", "right"] {
h.call("ui.view", json!({"view":view}));
let texts = painted_text(&mut h);
for text in ["13.00 mm", "X: 3.00 mm", "Y: 4.00 mm", "Z: 12.00 mm"] {
assert!(texts.iter().any(|t| t == text), "{view}, reverse={reverse}: {texts:?}");
}
let rects = component_rects();
for (i, a) in rects.iter().enumerate() {
assert!(rects.iter().skip(i + 1).all(|b| !a.intersects(*b)), "labels overlap in {view}: {rects:?}");
}
}
h.call("ui.set", json!({"perspective":true}));
h.call("ui.prefs", json!({"up_axis":"y"}));
let texts = painted_text(&mut h);
assert!(texts.contains(&"Z: 12.00 mm".to_string()), "view orientation does not change world-axis distances");
h.call("ui.key", json!({"key":"Escape"}));
assert!(crate::scenario::handles("measure:label").is_empty(), "all temporary dimensions disappear on close");
}
}
#[test]
fn measurement_axis_distances_keep_zero_components_readable_and_follow_units() {
let mut h = vertex_measurement([0., 0., 0.], [-3., 0., 0.]);
h.call("ui.view", json!({"view":"top"}));
let texts = painted_text(&mut h);
assert!(texts.contains(&"Y: 0.00 mm".into()) && texts.contains(&"Z: 0.00 mm".into()));
let rects = component_rects();
for (i, a) in rects.iter().enumerate() {
assert!(rects.iter().skip(i + 1).all(|b| !a.intersects(*b)), "zero-component labels must remain readable: {rects:?}");
}
h.app.run("document.units", json!({"units":"in"})).unwrap();
h.call("ui.prefs", json!({"length_decimals":3}));
let texts = painted_text(&mut h);
assert!(texts.contains(&"X: 0.1181 in".into()), "inch dimensions retain a meaningful fourth digit: {texts:?}");
assert!(texts.contains(&"Y: 0.000 in".into()) && texts.contains(&"Z: 0.000 in".into()));
}
#[test]
fn measurement_axis_distances_can_be_disabled_saved_and_restored_in_preferences() {
assert!(serde_json::from_value::<crate::prefs::Prefs>(json!({})).unwrap().measure_axis_distances, "existing settings get the default-on option");
let mut h = vertex_measurement([0., 0., 0.], [-3., 4., -12.]);
let measurement = result(&h).clone();
h.call("ui.prefs", json!({"open":"Graphics"}));
let checkbox = crate::scenario::rect_of("prefs:measure_axis_distances").unwrap().center();
h.call("ui.click", json!({"x":checkbox.x,"y":checkbox.y}));
assert!(!h.app.preferences.measure_axis_distances);
assert!(crate::scenario::handle_at("measure:label").is_some(), "total stays visible");
assert!(crate::scenario::handle_at("measure:label:x").is_none());
assert_eq!(*result(&h), measurement, "hiding the extra overlay does not change the measurement");
h.call("ui.prefs", json!({"button":"ok"}));
let texts = painted_text(&mut h);
assert!(!texts.iter().any(|text| text.starts_with("X: ") || text.starts_with("Y: ") || text.starts_with("Z: ")));
let mut restored = crate::SolveApp::new(Default::default(), Default::default());
restored.load_prefs(&h.app.prefs());
assert!(!restored.preferences.measure_axis_distances, "disabled preference survives restart");
h.call("ui.prefs", json!({"open":"Graphics","measure_axis_distances":true}));
assert!(crate::scenario::handle_at("measure:label:x").is_some());
h.call("ui.prefs", json!({"button":"cancel"}));
assert!(!h.app.preferences.measure_axis_distances, "Cancel restores the saved choice");
h.call("ui.prefs", json!({"open":"Graphics"}));
h.call("ui.prefs", json!({"button":"defaults"}));
assert!(h.app.preferences.measure_axis_distances);
h.call("ui.prefs", json!({"button":"apply"}));
h.call("ui.prefs", json!({"measure_axis_distances":false}));
h.call("ui.prefs", json!({"button":"cancel"}));
assert!(h.app.preferences.measure_axis_distances, "Cancel restores the last Apply");
}
+9 -2
View File
@@ -283,6 +283,8 @@ pub struct Prefs {
pub supersampling: f32, pub supersampling: f32,
/// Supersample only while the view holds still (full speed while orbiting). /// Supersample only while the view holds still (full speed while orbiting).
pub supersampling_adaptive: bool, pub supersampling_adaptive: bool,
/// Show world X/Y/Z component dimensions beside the total distance in Measure.
pub measure_axis_distances: bool,
/// Decimals shown for lengths and angles. /// Decimals shown for lengths and angles.
pub length_decimals: u8, pub length_decimals: u8,
pub angle_decimals: u8, pub angle_decimals: u8,
@@ -396,6 +398,7 @@ impl Default for Prefs {
display_quality: "normal".into(), display_quality: "normal".into(),
supersampling: 1.0, supersampling: 1.0,
supersampling_adaptive: true, supersampling_adaptive: true,
measure_axis_distances: true,
length_decimals: 2, length_decimals: 2,
angle_decimals: 1, angle_decimals: 1,
theme: "dark".into(), theme: "dark".into(),
@@ -618,6 +621,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
p.display_quality = d.display_quality; p.display_quality = d.display_quality;
p.supersampling = d.supersampling; p.supersampling = d.supersampling;
p.supersampling_adaptive = d.supersampling_adaptive; p.supersampling_adaptive = d.supersampling_adaptive;
p.measure_axis_distances = d.measure_axis_distances;
p.color_vision = d.color_vision; p.color_vision = d.color_vision;
app.ui.visual_style = 0; app.ui.visual_style = 0;
app.ui.show_grid = true; app.ui.show_grid = true;
@@ -1278,9 +1282,12 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
_ => "CPU (software)".to_string(), _ => "CPU (software)".to_string(),
}); });
ui.end_row(); ui.end_row();
// Colour vision (#778): its own section at the end of the page. ui.label(app.tr("settings.measure_axis_distances"));
ui.label(""); let response = ui.checkbox(&mut app.preferences.measure_axis_distances, "");
crate::scenario::publish_rect("prefs:measure_axis_distances", response.rect);
response.on_hover_text(app.tr("settings.measure_axis_distances_help"));
ui.end_row(); ui.end_row();
// Colour vision (#778): its own section at the end of the page.
ui.label(RichText::new(app.tr("ui.colour_vision")).strong()); ui.label(RichText::new(app.tr("ui.colour_vision")).strong());
ui.label(""); ui.label("");
ui.end_row(); ui.end_row();
+5
View File
@@ -243,6 +243,11 @@ retain trailing zeros. Component/canvas distance controls and inspection, parts-
and flat-pattern measurements follow design units too; parameter expressions and metric and flat-pattern measurements follow design units too; parameter expressions and metric
standard names retain their explicit authored units. standard names retain their explicit authored units.
Measure shows the currently selected distance (centre, minimum or maximum) and separate X, Y and Z
distances on the canvas. The component dimensions follow the world axes and remain visible
until the Measure dialog closes. To hide these extra lines and labels, turn off
**Preferences > Graphics > Show X/Y/Z distances**. The choice is saved between runs.
### Reviewing mirror, pattern, and offset results ### Reviewing mirror, pattern, and offset results
Mirror, rectangular/circular Pattern, and Offset show the active selection step in their Mirror, rectangular/circular Pattern, and Offset show the active selection step in their
+11
View File
@@ -97,6 +97,17 @@ read in the design's units, exactly as if it were typed into the dimension box:
unit regardless of the design, pass an expression with one: `"value": "50 mm"` or unit regardless of the design, pass an expression with one: `"value": "50 mm"` or
`"value": "0.5 in"`. Angles are degrees unless the expression says otherwise. `"value": "0.5 in"`. Angles are degrees unless the expression says otherwise.
The viewport also shows an X/Y/Z-aligned dimension chain between the endpoints of the
currently shown distance (centre, minimum or maximum), with each world-axis distance
labelled separately as an absolute value (including zero).
It uses the design's units and length precision, independently of the view or sketch plane.
Preferences → Graphics → **Show X/Y/Z distances** controls these extra lines and
labels (default on, saved between runs). Set `ui.prefs {"measure_axis_distances": false}`
to hide them while keeping the total distance and the dialog's ΔX/ΔY/ΔZ values. These
overlays remain visible until Measure closes.
Overlay handles `measure:label:x`, `measure:label:y` and `measure:label:z` identify the extra
labels; `prefs:measure_axis_distances` identifies the checkbox.
## UI ## UI
| Method | Params | Notes | | Method | Params | Notes |