From 2688ffbd6bf0093b54bb47d0096c304a6f9a914c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 30 Jul 2026 11:13:04 +0300 Subject: [PATCH] fix(ui): filter scales by drawing units Refs #578 --- src/app/view/mod.rs | 2 +- src/scene/mod.rs | 124 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index fcb3c68b1..5665389ce 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1434,7 +1434,7 @@ impl OpenCADStudio { layout_names: layout_names.clone(), polar_custom_input: &self.polar_custom_input, scale_is_model: is_model, - scale_list: tab.scene.scale_list(), + scale_list: tab.scene.scale_picker_list(), has_selection: !tab.scene.selected.is_empty(), selection_types: tab .scene diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f40b2f9bd..10a072844 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3425,14 +3425,17 @@ impl Scene { /// ratio (0.02 for "1:50"). Sorted smallest ratio first (1:100 … 1:1 … /// 10:1). Falls back to a standard ratio set when the drawing carries no /// scale list of its own, so the scale picker is always usable. (#154) - /// Standard fallback ratio set as (label, paper/drawing factor). Shown when - /// the drawing defines no annotation scales of its own, and materialised as - /// real `Scale` objects on demand by [`Scene::ensure_real_scale_list`]. - const DEFAULT_SCALES: &'static [(&'static str, f64)] = &[ + /// Standard fallback sets as (label, paper/drawing factor). The drawing's + /// INSUNITS selects metric ratios or imperial architectural labels. Used + /// when no file scales exist and materialised on demand by + /// [`Scene::ensure_real_scale_list`]. + const DEFAULT_METRIC_SCALES: &'static [(&'static str, f64)] = &[ + ("1:1000", 0.001), ("1:500", 0.002), ("1:200", 0.005), ("1:100", 0.01), ("1:50", 0.02), + ("1:25", 0.04), ("1:20", 0.05), ("1:10", 0.1), ("1:5", 0.2), @@ -3443,6 +3446,57 @@ impl Scene { ("10:1", 10.0), ]; + const DEFAULT_IMPERIAL_SCALES: &'static [(&'static 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\"", 1.0 / 4.0), + ("6\" = 1'-0\"", 1.0 / 2.0), + ("1'-0\" = 1'-0\"", 1.0), + ]; + + fn prefers_imperial_scales(&self) -> Option { + match self.document.header.insertion_units { + 1..=3 | 8..=10 | 21..=24 => Some(true), + 4..=7 | 11..=20 => Some(false), + 0 if self.document.header.measurement == 1 => Some(false), + _ => None, + } + } + + fn default_scales(&self) -> &'static [(&'static str, f64)] { + if self.prefers_imperial_scales() == Some(true) { + Self::DEFAULT_IMPERIAL_SCALES + } else { + Self::DEFAULT_METRIC_SCALES + } + } + + fn is_architectural_scale_name(name: &str) -> bool { + name.contains('=') && name.contains('"') && name.contains('\'') + } + + fn is_ratio_scale_name(name: &str) -> bool { + name.split_once(':') + .and_then(|(paper, drawing)| { + Some(( + paper.trim().parse::().ok()?, + drawing.trim().parse::().ok()?, + )) + }) + .is_some_and(|(paper, drawing)| paper > 0.0 && drawing > 0.0) + } + /// True when the drawing owns at least one annotation scale of its own /// (i.e. `scale_list` returns real objects rather than the fallback set). fn has_own_scales(&self) -> bool { @@ -3465,7 +3519,7 @@ impl Scene { if self.has_own_scales() { return false; } - for &(label, factor) in Self::DEFAULT_SCALES { + for &(label, factor) in self.default_scales() { // Fallback labels are "paper:drawing"; parse them for tidy whole // units, else derive drawing units from the factor (paper = 1). let (paper, drawing) = label @@ -3507,7 +3561,8 @@ impl Scene { // annotation / viewport scale picker would be empty and so appear // broken. Substitute the standard ratio set — file scales still // win whenever the drawing actually defines any. (#154) - list = Self::DEFAULT_SCALES + list = self + .default_scales() .iter() .map(|&(label, vp)| (label.to_string(), (1.0 / vp) as f32, vp)) .collect(); @@ -3515,6 +3570,63 @@ impl Scene { list } + /// Scale list for the status-bar picker. Some DWGs carry both the standard + /// metric and architectural sets in `ACAD_SCALELIST`; show only the family + /// matching INSUNITS while retaining custom names. The complete file list + /// remains available to the scale manager and is written back unchanged. + pub fn scale_picker_list(&self) -> Vec<(String, f32, f64)> { + let all = self.scale_list(); + let Some(imperial) = self.prefers_imperial_scales() else { + return all; + }; + + let mut visible: Vec<_> = all + .iter() + .filter(|(name, _, _)| { + let architectural = Self::is_architectural_scale_name(name); + let ratio = Self::is_ratio_scale_name(name); + if imperial { + architectural || (!architectural && !ratio) + } else { + ratio || (!architectural && !ratio) + } + }) + .cloned() + .collect(); + + let has_matching_family = visible.iter().any(|(name, _, _)| { + if imperial { + Self::is_architectural_scale_name(name) + } else { + Self::is_ratio_scale_name(name) + } + }); + if !has_matching_family { + visible.extend( + self.default_scales() + .iter() + .map(|&(label, vp)| (label.to_string(), (1.0 / vp) as f32, vp)), + ); + } + + // If the current scale belongs to the opposite family and has no + // equivalent factor in the visible family, keep that one active entry + // visible instead of silently hiding the drawing's current state. + if let Some(active) = all.iter().find(|(name, _, _)| { + name.eq_ignore_ascii_case(&self.document.header.current_annotation_scale) + }) { + let has_equivalent = visible + .iter() + .any(|(_, _, vp)| (vp - active.2).abs() < 0.001 * active.2.max(0.001)); + if !has_equivalent { + visible.push(active.clone()); + } + } + + visible.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)); + visible + } + /// Handle of the drawing's `ACAD_SCALELIST` dictionary, located robustly. /// /// The canonical path is the root named-objects dict → `"ACAD_SCALELIST"`,