diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index eb33b4ca0..caab4c5ef 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -2403,4 +2403,39 @@ mod find_replace_command_tests { let _ = app.dispatch_inquiry(cmd, i); } } + + #[test] + fn dist_command_respects_drawing_units_precision() { + let mut app = OpenCADStudio::new_for_test(); + let i = app.active_tab; + // Simulate changing linear precision to 0 via UNITS dialog apply + app.drawing_units = Some(crate::ui::window::drawing_units::State { + linear_format: 2, + linear_precision: 0, + angular_format: 0, + angular_precision: 0, + clockwise: false, + base_angle: "0".into(), + insertion_units: 4, + }); + let _ = app.update(Message::DrawingUnitsApply); + + // Run DIST command + let _ = app.dispatch_inquiry("DIST", i); + assert!(app.tabs[i].active_cmd.is_some()); + + // First point + let _ = app.tabs[i].active_cmd.as_mut().unwrap().on_point(glam::DVec3::new(0.0, 0.0, 0.0)); + // Second point + let res = app.tabs[i].active_cmd.as_mut().unwrap().on_point(glam::DVec3::new(10.0, 0.0, 0.0)); + match res { + crate::command::CmdResult::Measurement(msg) => { + assert!(msg.contains("Distance = 10"), "Expected 'Distance = 10', got: {msg}"); + assert!(msg.contains("Delta X = 10"), "Expected 'Delta X = 10', got: {msg}"); + assert!(msg.contains("Delta Y = 0"), "Expected 'Delta Y = 0', got: {msg}"); + assert!(msg.contains("Delta Z = 0"), "Expected 'Delta Z = 0', got: {msg}"); + } + _ => panic!("Expected CmdResult::Measurement"), + } + } } diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index a84c67381..b075bcec7 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -2505,6 +2505,13 @@ impl OpenCADStudio { if name == "PDMODE" || name == "PDSIZE" { self.tabs[i].scene.invalidate_point_dependencies(); } + if matches!(name.as_str(), "LUNITS" | "LUPREC" | "AUNITS" | "AUPREC") { + crate::entities::common::set_unit_context( + crate::entities::common::UnitContext::from_header( + &self.tabs[i].scene.document.header, + ), + ); + } // ORTHOMODE / OSMODE set the header directly; mirror them into // the live Ortho / running OSNAP so the constraint + status // bar follow and the save-time stamp doesn't revert them. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c638bb100..8edd3f181 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -283,6 +283,11 @@ impl OpenCADStudio { } pub fn update(&mut self, msg: Message) -> Task { + if let Some(tab) = self.tabs.get(self.active_tab) { + crate::entities::common::set_unit_context( + crate::entities::common::UnitContext::from_header(&tab.scene.document.header), + ); + } self.control_observe_user_message(&msg); let perf_started = crate::perf::enabled().then(Instant::now); let perf_label = perf_message_label(&msg); @@ -4596,6 +4601,9 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.tabs[i].scene.bump_geometry(); self.refresh_properties(); + crate::entities::common::set_unit_context( + crate::entities::common::UnitContext::from_header(&self.tabs[i].scene.document.header), + ); Task::none() } Message::ToleranceDialogField(field) => { @@ -4646,6 +4654,9 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.tabs[i].scene.bump_geometry(); self.refresh_properties(); + crate::entities::common::set_unit_context( + crate::entities::common::UnitContext::from_header(&self.tabs[i].scene.document.header), + ); let label = crate::modules::draw::units::linear_format_label(code); self.command_line .push_output(crate::tf!("Length format is now {label}.").as_ref()); diff --git a/src/entities/common.rs b/src/entities/common.rs index 2c012d226..136af184e 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -108,7 +108,7 @@ pub fn format_length(value: f64) -> String { /// Drop the sign from a formatted number whose digits all rounded to zero. A /// rotation leaves coordinates like -1e-15 behind, which read as `-0.0000`. -fn without_negative_zero(text: String) -> String { +pub fn without_negative_zero(text: String) -> String { match text.strip_prefix('-') { Some(rest) if rest.chars().any(|c| c.is_ascii_digit()) diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 645dc701c..2d50f8d13 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -169,10 +169,13 @@ impl AreaCommand { match measurement.perimeter { Some(perimeter) => crate::tr!( "area", "result", - area = format!("{:.4}", measurement.area), - perimeter = format!("{perimeter:.4}"), + area = crate::entities::common::format_area(measurement.area), + perimeter = crate::entities::common::format_length(perimeter), + ), + None => crate::tr!( + "area", "result-area-only", + area = crate::entities::common::format_area(measurement.area), ), - None => crate::tr!("area", "result-area-only", area = format!("{:.4}", measurement.area)), } } @@ -180,15 +183,15 @@ impl AreaCommand { match measurement.perimeter { Some(perimeter) => crate::tr!( "area", "running-result", - area = format!("{:.4}", measurement.area), - perimeter = format!("{perimeter:.4}"), - total_area = format!("{:.4}", self.total_area), - total_perimeter = format!("{:.4}", self.total_perimeter), + area = crate::entities::common::format_area(measurement.area), + perimeter = crate::entities::common::format_length(perimeter), + total_area = crate::entities::common::format_area(self.total_area), + total_perimeter = crate::entities::common::format_length(self.total_perimeter), ), None => crate::tr!( "area", "running-result-area-only", - area = format!("{:.4}", measurement.area), - total_area = format!("{:.4}", self.total_area), + area = crate::entities::common::format_area(measurement.area), + total_area = crate::entities::common::format_area(self.total_area), ), } } diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 74d4e3f03..2454ee470 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -4,6 +4,7 @@ use glam::DVec3; use crate::t; use crate::command::{CadCommand, CmdResult, WorkingPlane}; +use crate::entities::common::{format_length, unit_context, without_negative_zero}; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; pub struct DistCommand { @@ -55,12 +56,13 @@ impl CadCommand for DistCommand { let dist_xy = dx.hypot(dy); let angle_z = dz.atan2(dist_xy).to_degrees(); - let dist_s = format!("{dist:.4}"); - let angle_xy_s = format!("{angle_xy:.4}"); - let angle_z_s = format!("{angle_z:.4}"); - let dx_s = format!("{dx:.4}"); - let dy_s = format!("{dy:.4}"); - let dz_s = format!("{dz:.4}"); + let aprec = unit_context().auprec.max(0) as usize; + let dist_s = format_length(dist); + let angle_xy_s = without_negative_zero(format!("{angle_xy:.aprec$}")); + let angle_z_s = without_negative_zero(format!("{angle_z:.aprec$}")); + let dx_s = format_length(dx); + let dy_s = format_length(dy); + let dz_s = format_length(dz); let msg = t!( "Distance = %{dist}, Angle in XY Plane = %{angle_xy}°, Angle from XY Plane = %{angle_z}°\n Delta X = %{dx}, Delta Y = %{dy}, Delta Z = %{dz}", dist = dist_s, @@ -128,3 +130,84 @@ impl CadCommand for DistCommand { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["DIST"] }); // DistCommand + +#[cfg(test)] +mod tests { + use super::*; + use crate::command::CmdResult; + use crate::entities::common::{set_unit_context, UnitContext}; + + #[test] + fn dist_respects_precision_zero() { + let mut cmd = DistCommand::new(); + set_unit_context(UnitContext { + lunits: 2, + luprec: 0, + aunits: 0, + auprec: 0, + angbase: 0.0, + angdir_cw: false, + }); + + assert!(matches!(cmd.on_point(DVec3::new(0.0, 0.0, 0.0)), CmdResult::NeedPoint)); + let res = cmd.on_point(DVec3::new(10.0, 0.0, 0.0)); + match res { + CmdResult::Measurement(msg) => { + assert!(msg.contains("Distance = 10"), "Expected 'Distance = 10' in '{msg}'"); + assert!(msg.contains("Delta X = 10"), "Expected 'Delta X = 10' in '{msg}'"); + assert!(msg.contains("Delta Y = 0"), "Expected 'Delta Y = 0' in '{msg}'"); + assert!(msg.contains("Delta Z = 0"), "Expected 'Delta Z = 0' in '{msg}'"); + assert!(msg.contains("Angle in XY Plane = 0°"), "Expected 'Angle in XY Plane = 0°' in '{msg}'"); + } + _ => panic!("Expected CmdResult::Measurement"), + } + } + + #[test] + fn dist_respects_precision_four() { + let mut cmd = DistCommand::new(); + set_unit_context(UnitContext { + lunits: 2, + luprec: 4, + aunits: 0, + auprec: 2, + angbase: 0.0, + angdir_cw: false, + }); + + assert!(matches!(cmd.on_point(DVec3::new(0.0, 0.0, 0.0)), CmdResult::NeedPoint)); + let res = cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + match res { + CmdResult::Measurement(msg) => { + assert!(msg.contains("Distance = 11.1803"), "Expected 'Distance = 11.1803' in '{msg}'"); + assert!(msg.contains("Delta X = 10.0000"), "Expected 'Delta X = 10.0000' in '{msg}'"); + assert!(msg.contains("Delta Y = 5.0000"), "Expected 'Delta Y = 5.0000' in '{msg}'"); + assert!(msg.contains("Angle in XY Plane = 26.57°"), "Expected 'Angle in XY Plane = 26.57°' in '{msg}'"); + } + _ => panic!("Expected CmdResult::Measurement"), + } + } + + #[test] + fn dist_respects_architectural_units() { + let mut cmd = DistCommand::new(); + set_unit_context(UnitContext { + lunits: 4, // Architectural + luprec: 4, + aunits: 0, + auprec: 0, + angbase: 0.0, + angdir_cw: false, + }); + + assert!(matches!(cmd.on_point(DVec3::new(0.0, 0.0, 0.0)), CmdResult::NeedPoint)); + let res = cmd.on_point(DVec3::new(18.0, 0.0, 0.0)); + match res { + CmdResult::Measurement(msg) => { + // 18 inches = 1'-6" + assert!(msg.contains("1'-6\""), "Expected 1'-6\" in '{msg}'"); + } + _ => panic!("Expected CmdResult::Measurement"), + } + } +} diff --git a/src/modules/draw/inquiry/id.rs b/src/modules/draw/inquiry/id.rs index ef2af86ce..8a9be8ef7 100644 --- a/src/modules/draw/inquiry/id.rs +++ b/src/modules/draw/inquiry/id.rs @@ -35,9 +35,9 @@ impl CadCommand for IdCommand { let x = local.x; let y = local.y; let z = local.z; - let x_s = format!("{x:.4}"); - let y_s = format!("{y:.4}"); - let z_s = format!("{z:.4}"); + let x_s = crate::entities::common::format_length(x); + let y_s = crate::entities::common::format_length(y); + let z_s = crate::entities::common::format_length(z); let msg = t!( "X = %{x}, Y = %{y}, Z = %{z}", x = x_s, diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 73a355ff7..24b07c052 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -124,11 +124,12 @@ impl MeasureGeomCommand { let dy = delta.y; let dz = delta.z; let angle_xy = dy.atan2(dx).to_degrees(); - let dist_s = format!("{dist:.4}"); - let angle_xy_s = format!("{angle_xy:.4}"); - let dx_s = format!("{dx:.4}"); - let dy_s = format!("{dy:.4}"); - let dz_s = format!("{dz:.4}"); + let aprec = crate::entities::common::unit_context().auprec.max(0) as usize; + let dist_s = crate::entities::common::format_length(dist); + let angle_xy_s = crate::entities::common::without_negative_zero(format!("{angle_xy:.aprec$}")); + let dx_s = crate::entities::common::format_length(dx); + let dy_s = crate::entities::common::format_length(dy); + let dz_s = crate::entities::common::format_length(dz); t!( "Distance = %{dist}, Angle in XY Plane = %{angle_xy}°\n Delta X = %{dx}, Delta Y = %{dy}, Delta Z = %{dz}", dist = dist_s, @@ -154,8 +155,8 @@ impl MeasureGeomCommand { perimeter += (points[(idx + 1) % n] - points[idx]).length(); } let area = (area_sum * 0.5).abs(); - let area_s = format!("{area:.4}"); - let perimeter_s = format!("{perimeter:.4}"); + let area_s = crate::entities::common::format_area(area); + let perimeter_s = crate::entities::common::format_length(perimeter); t!( "Area = %{area}, Perimeter = %{perimeter}", area = area_s, @@ -170,12 +171,14 @@ impl MeasureGeomCommand { let vb = b - vertex; let la = va.length(); let lb = vb.length(); + let aprec = crate::entities::common::unit_context().auprec.max(0) as usize; if la == 0.0 || lb == 0.0 { - return t!("Angle = 0.0000° (degenerate rays)").into_owned(); + let zero = crate::entities::common::without_negative_zero(format!("{:.aprec$}", 0.0)); + return t!("Angle = %{angle}° (degenerate rays)", angle = zero).into_owned(); } let cos = (va.dot(vb) / (la * lb)).clamp(-1.0, 1.0); let angle = cos.acos().to_degrees(); - let angle_s = format!("{angle:.4}"); + let angle_s = crate::entities::common::without_negative_zero(format!("{angle:.aprec$}")); t!("Angle = %{angle}°", angle = angle_s).into_owned() } } @@ -274,8 +277,8 @@ impl CadCommand for MeasureGeomCommand { match self.picked.as_ref().and_then(Self::radius_of) { Some(radius) => { let diameter = radius * 2.0; - let radius_s = format!("{radius:.4}"); - let diameter_s = format!("{diameter:.4}"); + let radius_s = crate::entities::common::format_length(radius); + let diameter_s = crate::entities::common::format_length(diameter); CmdResult::Measurement( t!( "Radius = %{radius}, Diameter = %{diameter}",