feat(scale): drive scales from the drawing's scale list

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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-04 16:18:01 +03:00
1 parent b5ce376d5c
commit edc8cbc969
8 files changed
+128 -30

No files matched your search

Generated
+2 -2
View File
@@ -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",
]
+3 -2
View File
@@ -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" }
+28
View File
@@ -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<String> =
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.
+37 -4
View File
@@ -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);
+1
View File
@@ -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()
+18 -2
View File
@@ -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<f64> {
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::<f64>().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;
+31
View File
@@ -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<acadrust::Handle>)> {
if self.current_layout == "Model" {
+8 -20
View File
@@ -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<f64>,
file_scales: Vec<(String, f32, f64)>,
) -> Element<'static, Message> {
let rows: Vec<Element<'static, Message>> = COMMON_SCALES
.iter()
.map(|&(label, anno_scale, vp_scale)| {
let rows: Vec<Element<'static, Message>> = 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 {