diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index c9fdea7..b9f537e 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -373,6 +373,8 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) { app.canvas.list = None; } 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. // Ellipsis and a tooltip keep either value readable without covering layout tabs. diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 32a855a..89bdfd4 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -72,6 +72,7 @@ fn every_marked_ui_literal_has_a_translation() { include_str!("../palettes.rs"), include_str!("../parametric.rs"), include_str!("../quick.rs"), + include_str!("../vpscale.rs"), ]; let mut missing = BTreeSet::new(); for source in sources { diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a7d3068..6465a25 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -430,6 +430,7 @@ Loaded linetypes Завантажені типи ліній Lock Заблокувати Lock Layer Заблокувати шар + Lock viewport Заблокувати видовий екран Lock/Unlock Заблокувати/розблокувати Loft Тіло за перерізами MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Не пов'язано з Autodesk, Inc. @@ -965,6 +966,7 @@ Unisolate Layer Скасувати ізоляцію шарів Units... Одиниці... Unlock Layer Розблокувати шар + Unlock viewport Розблокувати видовий екран Unmerge Cells Розділити комірки Unsaved Layer State Незбережений стан шарів Up Угору @@ -992,6 +994,8 @@ Viewport Видовий екран Viewport Layer Freeze Заморозити шар у видовому екрані Viewport Properties Властивості видового екрана + Viewport is locked Видовий екран заблоковано + Viewport scale Масштаб видового екрана Viewport {id} Видовий екран {id} Viewports Видові екрани W Зх diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b5bad15..4b5acb9 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -33,6 +33,7 @@ pub mod plotstyles; pub mod quick; pub mod theme; pub mod viewcube; +pub mod vpscale; pub mod exchange; diff --git a/crates/ui-egui/src/vpscale.rs b/crates/ui-egui/src/vpscale.rs new file mode 100644 index 0000000..a4720f9 --- /dev/null +++ b/crates/ui-egui/src/vpscale.rs @@ -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), "?"); + } +}