Status bar: viewport scale list and lock toggle (fixes #453) (#462)

Squash of PR #462 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-11 11:36:16 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d42313cde2
commit 6aa7c03fad
5 changed files with 181 additions and 0 deletions
+2
View File
@@ -373,6 +373,8 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
app.canvas.list = None; app.canvas.list = None;
} }
coord_right = br.left() - 10.0; coord_right = br.left() - 10.0;
// Scale and lock of the active or selected viewport.
coord_right = crate::vpscale::status(app, ui, coord_right + 4.0, r.center().y) - 10.0;
} }
// A transient message occupies the same bounded readout slot as coordinates. // A transient message occupies the same bounded readout slot as coordinates.
// Ellipsis and a tooltip keep either value readable without covering layout tabs. // Ellipsis and a tooltip keep either value readable without covering layout tabs.
+1
View File
@@ -72,6 +72,7 @@ fn every_marked_ui_literal_has_a_translation() {
include_str!("../palettes.rs"), include_str!("../palettes.rs"),
include_str!("../parametric.rs"), include_str!("../parametric.rs"),
include_str!("../quick.rs"), include_str!("../quick.rs"),
include_str!("../vpscale.rs"),
]; ];
let mut missing = BTreeSet::new(); let mut missing = BTreeSet::new();
for source in sources { for source in sources {
+4
View File
@@ -430,6 +430,7 @@
Loaded linetypes Завантажені типи ліній Loaded linetypes Завантажені типи ліній
Lock Заблокувати Lock Заблокувати
Lock Layer Заблокувати шар Lock Layer Заблокувати шар
Lock viewport Заблокувати видовий екран
Lock/Unlock Заблокувати/розблокувати Lock/Unlock Заблокувати/розблокувати
Loft Тіло за перерізами Loft Тіло за перерізами
MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Не пов'язано з Autodesk, Inc. MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Не пов'язано з Autodesk, Inc.
@@ -965,6 +966,7 @@
Unisolate Layer Скасувати ізоляцію шарів Unisolate Layer Скасувати ізоляцію шарів
Units... Одиниці... Units... Одиниці...
Unlock Layer Розблокувати шар Unlock Layer Розблокувати шар
Unlock viewport Розблокувати видовий екран
Unmerge Cells Розділити комірки Unmerge Cells Розділити комірки
Unsaved Layer State Незбережений стан шарів Unsaved Layer State Незбережений стан шарів
Up Угору Up Угору
@@ -992,6 +994,8 @@
Viewport Видовий екран Viewport Видовий екран
Viewport Layer Freeze Заморозити шар у видовому екрані Viewport Layer Freeze Заморозити шар у видовому екрані
Viewport Properties Властивості видового екрана Viewport Properties Властивості видового екрана
Viewport is locked Видовий екран заблоковано
Viewport scale Масштаб видового екрана
Viewport {id} Видовий екран {id} Viewport {id} Видовий екран {id}
Viewports Видові екрани Viewports Видові екрани
W Зх W Зх
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
430
431
432
433
434
435
436
966
967
968
969
970
971
972
994
995
996
997
998
999
1000
1001
+1
View File
@@ -33,6 +33,7 @@ pub mod plotstyles;
pub mod quick; pub mod quick;
pub mod theme; pub mod theme;
pub mod viewcube; pub mod viewcube;
pub mod vpscale;
pub mod exchange; pub mod exchange;
+173
View File
@@ -0,0 +1,173 @@
//! The status bar's viewport scale list and lock toggle (layouts only): they show the scale of
//! the active viewport (MSPACE) or of the one selected viewport, and change it through
//! `viewport.set`.
use cadcraft_doc::{EntityKind, Handle, Space, Viewport};
use egui::{Rect, Sense, pos2, vec2};
use serde_json::json;
use crate::CadApp;
use crate::icons::{self, Icon};
use crate::theme::Tokens;
/// Standard ratio scales (paper units : model units).
pub const RATIOS: &[(&str, f64)] = &[
("1:1", 1.0),
("1:2", 0.5),
("1:4", 0.25),
("1:5", 0.2),
("1:8", 0.125),
("1:10", 0.1),
("1:16", 1.0 / 16.0),
("1:20", 0.05),
("1:30", 1.0 / 30.0),
("1:40", 0.025),
("1:50", 0.02),
("1:100", 0.01),
("1:200", 0.005),
("1:500", 0.002),
("1:1000", 0.001),
("2:1", 2.0),
("4:1", 4.0),
("8:1", 8.0),
("10:1", 10.0),
("100:1", 100.0),
];
/// Architectural scales for imperial drawings (paper inches per model inch).
pub const ARCHITECTURAL: &[(&str, f64)] = &[
("1/128\" = 1'-0\"", 1.0 / 1536.0),
("1/64\" = 1'-0\"", 1.0 / 768.0),
("1/32\" = 1'-0\"", 1.0 / 384.0),
("1/16\" = 1'-0\"", 1.0 / 192.0),
("3/32\" = 1'-0\"", 1.0 / 128.0),
("1/8\" = 1'-0\"", 1.0 / 96.0),
("3/16\" = 1'-0\"", 1.0 / 64.0),
("1/4\" = 1'-0\"", 1.0 / 48.0),
("3/8\" = 1'-0\"", 1.0 / 32.0),
("1/2\" = 1'-0\"", 1.0 / 24.0),
("3/4\" = 1'-0\"", 1.0 / 16.0),
("1\" = 1'-0\"", 1.0 / 12.0),
("1-1/2\" = 1'-0\"", 1.0 / 8.0),
("3\" = 1'-0\"", 0.25),
("6\" = 1'-0\"", 0.5),
("1'-0\" = 1'-0\"", 1.0),
];
/// The scales offered for a drawing: ratios, plus architectural scales when it is imperial.
pub fn scales(metric: bool) -> Vec<(&'static str, f64)> {
let mut v = RATIOS.to_vec();
if !metric {
v.extend_from_slice(ARCHITECTURAL);
}
v
}
fn same(a: f64, b: f64) -> bool {
(a - b).abs() <= b.abs() * 1e-6
}
/// How a scale reads: a standard name, else a ratio such as `1:37.5`.
pub fn label(scale: f64, metric: bool) -> String {
if !(scale.is_finite() && scale > 0.0) {
return "?".into();
}
if let Some((n, _)) = scales(metric).into_iter().find(|(_, v)| same(*v, scale)) {
return n.to_string();
}
let num = |x: f64| {
let r = (x * 1000.0).round() / 1000.0;
if r.fract() == 0.0 { format!("{r:.0}") } else { format!("{r}") }
};
if scale >= 1.0 { format!("{}:1", num(scale)) } else { format!("1:{}", num(1.0 / scale)) }
}
/// The viewport the control acts on: the active one (MSPACE), else the single selected one.
pub fn target(app: &CadApp) -> Option<(Handle, Viewport)> {
let st = app.session.state().ok()?;
if !matches!(st.space, Space::Paper(_)) {
return None;
}
if let Some(a) = st.active_viewport() {
return Some(a);
}
let sel = app.session.selection();
let [h] = sel.as_slice() else { return None };
match &st.doc.space(&st.space)?.get(*h)?.kind {
EntityKind::Viewport(v) if v.id != 1 => Some((*h, v.clone())),
_ => None,
}
}
/// Draw the scale list and lock toggle ending at `right`; returns their left edge.
pub fn status(app: &mut CadApp, ui: &mut egui::Ui, right: f32, cy: f32) -> f32 {
let Some((h, vp)) = target(app) else { return right };
let t = Tokens::get();
let metric = app.session.doc().is_ok_and(|d| cadcraft_render::paper::paper_unit_mm(d) == 1.0);
let scale = if vp.view_height > 0.0 { vp.height / vp.view_height } else { 0.0 };
let p = ui.painter().clone();
// Lock toggle.
let lr = Rect::from_min_size(pos2(right - 20.0, cy - 9.0), vec2(18.0, 18.0));
let lresp = ui.interact(lr, ui.id().with("vp_lock"), Sense::click());
if lresp.hovered() {
p.rect_filled(lr, 3.0, t.control_hover.gamma_multiply(0.5));
}
icons::paint(&p, lr.shrink(2.0), if vp.locked { Icon::Lock } else { Icon::Unlock }, false);
let tip = if vp.locked { crate::tl!("Unlock viewport") } else { crate::tl!("Lock viewport") };
if lresp.on_hover_text(tip).clicked() {
let r = app.run("viewport.set", json!({ "handle": h.hex(), "locked": !vp.locked }));
app.canvas.list = None;
if let Err(e) = r {
app.status = Some((e, crate::now_ms()));
}
}
// Scale list.
let text = label(scale, metric);
let g = p.layout_no_wrap(text, crate::theme::small(), if vp.locked { t.text_dim } else { t.text });
let w = g.size().x + 22.0;
let sr = Rect::from_min_max(pos2(lr.left() - 4.0 - w, cy - 9.0), pos2(lr.left() - 4.0, cy + 9.0));
let sresp = ui.interact(sr, ui.id().with("vp_scale"), Sense::click());
p.rect_filled(sr, 3.0, if sresp.hovered() { t.control_hover } else { t.control.gamma_multiply(0.5) });
p.galley(pos2(sr.left() + 6.0, sr.center().y - g.size().y / 2.0), g, t.text);
let (ax, ay) = (sr.right() - 9.0, sr.center().y);
p.line_segment([pos2(ax - 3.0, ay - 1.5), pos2(ax, ay + 1.5)], egui::Stroke::new(1.2, t.text_dim));
p.line_segment([pos2(ax, ay + 1.5), pos2(ax + 3.0, ay - 1.5)], egui::Stroke::new(1.2, t.text_dim));
let sresp = sresp.on_hover_text(if vp.locked { crate::tl!("Viewport is locked") } else { crate::tl!("Viewport scale") });
let mut pick = None;
if !vp.locked {
egui::Popup::from_toggle_button_response(&sresp).width(150.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
for (name, v) in scales(metric) {
if ui.selectable_label(same(v, scale), name).clicked() {
pick = Some(v);
ui.close();
}
}
});
});
}
if let Some(v) = pick {
let r = app.run("viewport.set", json!({ "handle": h.hex(), "scale": v }));
app.canvas.list = None;
if let Err(e) = r {
app.status = Some((e, crate::now_ms()));
}
}
sr.left()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn labels() {
assert_eq!(label(0.02, true), "1:50");
assert_eq!(label(1.0 / 48.0, false), "1/4\" = 1'-0\"");
assert_eq!(label(1.0 / 48.0, true), "1:48");
assert_eq!(label(1.0 / 37.5, true), "1:37.5");
assert_eq!(label(3.0, true), "3:1");
assert_eq!(label(f64::NAN, true), "?");
assert_eq!(label(0.0, true), "?");
}
}