fix(ui): filter scales by drawing units

Refs #578
This commit is contained in:
Hakan Seven 2026-07-30 11:13:04 +03:00
commit 2688ffbd6b
2 changed files with 119 additions and 7 deletions

View file

@ -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

View file

@ -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<bool> {
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::<f64>().ok()?,
drawing.trim().parse::<f64>().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"`,