From edc8cbc9691712d7780f9c33d6c6115870a496fb Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 4 Jun 2026 16:18:01 +0300 Subject: [PATCH] feat(scale): drive scales from the drawing's scale list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the model-space annotation scale from the standard CANNOSCALE / CANNOSCALEVALUE header variables instead of a user-variable hack, and populate both the status-bar scale picker and the viewport Scale property purely from the drawing's ACAD_SCALELIST — no built-in fallback set. Xref-derived scales (`_XREF` suffix, `name|` prefix) are filtered out so only the host drawing's own scales appear. Bumps the patched acadrust to the commit that adds CANNOSCALE/ CANNOSCALEVALUE read/write. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 4 ++-- Cargo.toml | 5 +++-- src/app/properties.rs | 28 +++++++++++++++++++++++++++ src/app/update.rs | 41 ++++++++++++++++++++++++++++++++++++---- src/app/view.rs | 1 + src/entities/viewport.rs | 20 ++++++++++++++++++-- src/scene/mod.rs | 31 ++++++++++++++++++++++++++++++ src/ui/scale_popup.rs | 28 ++++++++------------------- 8 files changed, 128 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4613d3b7..57b3d5467 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.3.4" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#535247755829ff7971d749a424fb2134df34f6d4" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#041df8c0568ea1242a39e22f2d04e770317bdf7c" dependencies = [ "ahash", "anyhow", @@ -3177,7 +3177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", - "memchr 1.0.2", + "memchr 2.8.1", "nom 8.0.0", ] diff --git a/Cargo.toml b/Cargo.toml index 0460ea683..333176b31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window [patch.crates-io] # Tracks HakanSeven12/acadrust main, which carries the hatch -# boundary-handle cap, raster-image/wipeout clip-vertex fix, and 3DFACE -# Z-decode fix on top of upstream. Revert to upstream once those PRs land. +# boundary-handle cap, raster-image/wipeout clip-vertex fix, 3DFACE +# Z-decode fix, and CANNOSCALE/CANNOSCALEVALUE header support on top of +# upstream. Revert to upstream once those PRs land. acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "main" } diff --git a/src/app/properties.rs b/src/app/properties.rs index edc029e69..dc489a6ec 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -144,6 +144,34 @@ impl OpenCADStudio { }); } } + + // Drive the viewport scale picker from the drawing's + // own scale list instead of a built-in set. + let file_scales = self.tabs[i].scene.scale_list(); + if !file_scales.is_empty() { + let eff = crate::scene::vp_effective_scale( + vp.custom_scale, + vp.view_height, + vp.height, + ); + let selected = file_scales + .iter() + .find(|(_, _, f)| (f - eff).abs() < 0.001 * f.max(0.001)) + .map(|(n, _, _)| n.clone()) + .unwrap_or_default(); + let options: Vec = + file_scales.iter().map(|(n, _, _)| n.clone()).collect(); + if let Some(geom) = sections.last_mut() { + if let Some(prop) = + geom.props.iter_mut().find(|p| p.field == "vscale_std") + { + prop.value = crate::scene::object::PropValue::Choice { + selected, + options, + }; + } + } + } } // Inject DimStyle picker for Dimension entities. diff --git a/src/app/update.rs b/src/app/update.rs index 07a33bf56..c00828b7f 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -163,9 +163,12 @@ impl OpenCADStudio { self.tabs[i].current_path = Some(path.clone()); self.tabs[i].scene.document = doc; - let stored = self.tabs[i].scene.document.header.user_real1; + // Current model-space annotation scale comes from the drawing's + // CANNOSCALEVALUE (paper/drawing factor); the multiplier we use + // for text/dim sizing is its inverse (1:50 -> 0.02 -> 50.0). + let cannoscale_value = self.tabs[i].scene.document.header.annotation_scale_value; self.tabs[i].scene.annotation_scale = - if stored > 1e-9 { stored as f32 } else { 1.0 }; + if cannoscale_value > 1e-9 { (1.0 / cannoscale_value) as f32 } else { 1.0 }; // Auto-resolve XREFs relative to the opened file's directory. if let Some(base_dir) = path.parent() { @@ -632,8 +635,7 @@ impl OpenCADStudio { let (_, version) = crate::io::parse_save_format(&self.save_dialog_format); let close = self.close_save_dialog_window(); let i = self.active_tab; - self.tabs[i].scene.document.header.user_real1 = - self.tabs[i].scene.annotation_scale as f64; + sync_annotation_scale_header(&mut self.tabs[i].scene); match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) { Ok(()) => { self.command_line @@ -7691,6 +7693,37 @@ impl OpenCADStudio { /// Parse a scale string like "1:50" or "2:1" into (numerator, denominator). /// Returns (1.0, 1.0) for "Fit" or unknown formats. +/// Sync the model-space annotation scale into the standard CANNOSCALE / +/// CANNOSCALEVALUE header variables before a save, so the scale round-trips +/// through the file (and is read correctly by other CAD applications). +fn sync_annotation_scale_header(scene: &mut Scene) { + let anno = scene.annotation_scale; + let value = if anno.abs() > 1e-9 { 1.0 / anno as f64 } else { 1.0 }; + // Prefer the name of a matching scale already in the drawing's list; + // fall back to a formatted ratio when none matches. + let name = scene + .scale_list() + .into_iter() + .find(|(_, a, _)| (a - anno).abs() < 0.001 * anno.max(0.001)) + .map(|(n, _, _)| n) + .unwrap_or_else(|| format_annotation_scale_name(anno)); + let hdr = &mut scene.document.header; + hdr.current_annotation_scale = name; + hdr.annotation_scale_value = value; +} + +/// Format an annotation-scale multiplier as a ratio name: 50.0 -> "1:50", +/// 0.5 -> "2:1", 1.0 -> "1:1". +fn format_annotation_scale_name(anno: f32) -> String { + if anno >= 1.0 { + format!("1:{}", anno.round() as i64) + } else if anno > 0.0 { + format!("{}:1", (1.0 / anno).round() as i64) + } else { + "1:1".to_string() + } +} + fn parse_plot_scale(s: &str) -> (f64, f64) { if s == "Fit" { return (1.0, 1.0); diff --git a/src/app/view.rs b/src/app/view.rs index 1717b5fd0..111fbebc0 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1163,6 +1163,7 @@ impl OpenCADStudio { is_model, tab.scene.annotation_scale, tab.scene.first_viewport_scale(), + tab.scene.scale_list(), ) } else { iced::widget::Space::new().width(0).height(0).into() diff --git a/src/entities/viewport.rs b/src/entities/viewport.rs index 5c6eef377..e85a46196 100644 --- a/src/entities/viewport.rs +++ b/src/entities/viewport.rs @@ -25,6 +25,20 @@ const STANDARD_SCALES: &[(&str, f64)] = &[ ("10:1", 10.0), ]; +/// Parse a scale ratio name to its paper/drawing factor: "1:50" -> 0.02, +/// "2:1" -> 2.0. Also accepts a plain decimal ("0.02"). +fn parse_scale_ratio(label: &str) -> Option { + if let Some((a, b)) = label.split_once(':') { + let n: f64 = a.trim().parse().ok()?; + let d: f64 = b.trim().parse().ok()?; + if d.abs() > 1e-12 { + return Some(n / d); + } + return None; + } + label.trim().parse::().ok() +} + fn scale_label(scale: f64) -> String { for (label, val) in STANDARD_SCALES { if (scale - val).abs() < val * 0.01 { @@ -411,9 +425,11 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) { _ => {} } - // Standard scale picker. + // Scale picker. The label is a ratio name from the drawing's scale list + // ("1:50", "2:1"); parse it to the paper/drawing factor so any named + // scale resolves, not just the built-in set. if field == "vscale_std" { - if let Some(&(_, scale)) = STANDARD_SCALES.iter().find(|(label, _)| *label == value) { + if let Some(scale) = parse_scale_ratio(value) { vp.custom_scale = scale; if scale > 1e-9 { vp.view_height = vp.height / scale; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 3f0e3814a..c6c5ed65e 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1225,6 +1225,37 @@ impl Scene { }) } + /// Annotation/viewport scales defined in the drawing's scale list + /// (the `ACAD_SCALELIST` dictionary), as `(label, annotation_multiplier, + /// viewport_factor)`. The annotation multiplier sizes model-space + /// text/dims (50.0 for "1:50"); the viewport factor is the paper/drawing + /// ratio (0.02 for "1:50"). Sorted smallest ratio first (1:100 … 1:1 … + /// 10:1). Empty when the drawing carries no scale list — the caller + /// substitutes its built-in defaults. + pub fn scale_list(&self) -> Vec<(String, f32, f64)> { + let mut list: Vec<(String, f32, f64)> = self + .document + .objects + .values() + .filter_map(|o| match o { + // Skip xref-derived scales. Scales pulled in from an external + // reference get an "_XREF" suffix ("1:50_XREF"); unbound + // dependent symbols carry a "xref|name" prefix. Neither + // belongs to this drawing's own scale list. + ObjectType::Scale(s) + if !s.is_temporary + && !s.name.contains('|') + && !s.name.to_ascii_uppercase().ends_with("_XREF") => + { + Some((s.name.clone(), s.inverse_factor() as f32, s.factor())) + } + _ => None, + }) + .collect(); + list.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)); + list + } + /// List of user viewports in the current layout: (handle, label, frozen_layer_handles). pub fn viewport_list(&self) -> Vec<(acadrust::Handle, String, Vec)> { if self.current_layout == "Model" { diff --git a/src/ui/scale_popup.rs b/src/ui/scale_popup.rs index 5874affc1..bfc964f2b 100644 --- a/src/ui/scale_popup.rs +++ b/src/ui/scale_popup.rs @@ -7,35 +7,23 @@ use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; -/// (label, annotation_scale_multiplier, viewport_scale_factor) -/// annotation_scale = multiplier for text/dim sizes (50.0 for "1:50") -/// vp_scale = custom_scale value on the Viewport entity (0.02 for "1:50") -const COMMON_SCALES: &[(&str, f32, f64)] = &[ - ("1:100", 100.0, 0.01), - ("1:50", 50.0, 0.02), - ("1:20", 20.0, 0.05), - ("1:10", 10.0, 0.10), - ("1:5", 5.0, 0.20), - ("1:2", 2.0, 0.50), - ("1:1", 1.0, 1.00), - ("2:1", 0.5, 2.00), - ("5:1", 0.2, 5.00), - ("10:1", 0.1, 10.00), -]; - /// Full-screen overlay: transparent click-catcher + scale list panel pinned bottom-right. /// /// - `is_model`: true = model space (dispatches SetAnnotationScale), false = paper space (SetViewportScale). /// - `current_anno_scale`: current annotation_scale from Scene (used to highlight active row in model space). /// - `viewport_scale`: current effective vp scale, view_height-first (used to highlight in paper space). +/// - `file_scales`: scale list read from the drawing (`ACAD_SCALELIST`). Only +/// scales actually stored in the file are shown; the picker never injects +/// scales of its own. pub fn scale_popup_overlay( is_model: bool, current_anno_scale: f32, viewport_scale: Option, + file_scales: Vec<(String, f32, f64)>, ) -> Element<'static, Message> { - let rows: Vec> = COMMON_SCALES - .iter() - .map(|&(label, anno_scale, vp_scale)| { + let rows: Vec> = file_scales + .into_iter() + .map(|(label, anno_scale, vp_scale)| { let active = if is_model { (current_anno_scale - anno_scale).abs() < 0.001 * current_anno_scale.max(0.001) } else { @@ -81,7 +69,7 @@ pub fn scale_popup_overlay( .into() } -fn scale_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> { +fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> { let check = text(if active { "✓" } else { " " }) .size(11) .color(if active {