diff --git a/src/app/properties.rs b/src/app/properties.rs index 6f74fc953..a8080ef5c 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -15,6 +15,19 @@ impl H7CAD { let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf); let selected_group = self.tabs[i].properties.selected_group.clone(); + // Seed the per-thread unit context from the document header so the + // entity property builders (which only see f64 values) can format + // lengths/angles per LUNITS / LUPREC / AUNITS / AUPREC. + { + let h = &self.tabs[i].scene.document.header; + crate::entities::common::set_unit_context(crate::entities::common::UnitContext { + lunits: h.linear_unit_format, + luprec: h.linear_unit_precision, + aunits: h.angular_unit_format, + auprec: h.angular_unit_precision, + }); + } + let layer_names: Vec = self.tabs[i] .scene .document @@ -428,6 +441,34 @@ impl H7CAD { entity.as_entity_mut().set_layer(layer.clone()); } + // INSUNITS: when inserting a block whose BlockRecord.units differ + // from the host's header.insertion_units, scale the new INSERT so + // 1 source-unit equals the matching host length. When either side + // is unitless (0) AutoCAD falls back to MEASUREMENT (0 = Imperial / + // inches, 1 = Metric / mm); honour the same fallback. + if let acadrust::EntityType::Insert(ref mut ins) = entity { + let header = &self.tabs[i].scene.document.header; + let measurement_fallback = if header.measurement == 1 { 4 } else { 1 }; + let host_raw = header.insertion_units; + let host_units = if host_raw == 0 { measurement_fallback } else { host_raw }; + let src_raw = self.tabs[i] + .scene + .document + .block_records + .get(&ins.block_name) + .map(|br| br.units) + .unwrap_or(0); + let src_units = if src_raw == 0 { measurement_fallback } else { src_raw }; + if src_units != host_units { + let ratio = insunits_to_mm(src_units) / insunits_to_mm(host_units); + if ratio.is_finite() && (ratio - 1.0).abs() > 1e-9 { + ins.set_x_scale(ratio); + ins.set_y_scale(ratio); + ins.set_z_scale(ratio); + } + } + } + crate::scene::dispatch::apply_color(&mut entity, self.ribbon.active_color); crate::scene::dispatch::apply_common_prop( &mut entity, @@ -637,3 +678,32 @@ fn merge_prop_value( _ => left.clone(), } } + +/// Convert INSUNITS (DXF group 70) to millimetres. +/// 0 = unitless / unknown: returns 1.0 so the caller treats it as "do not scale". +fn insunits_to_mm(code: i16) -> f64 { + match code { + 1 => 25.4, // Inches + 2 => 304.8, // Feet + 3 => 1_609_344.0, // Miles + 4 => 1.0, // Millimeters + 5 => 10.0, // Centimeters + 6 => 1_000.0, // Meters + 7 => 1_000_000.0, // Kilometers + 8 => 0.000_025_4, // Microinches + 9 => 0.025_4, // Mils + 10 => 914.4, // Yards + 11 => 1.0e-7, // Angstroms + 12 => 1.0e-6, // Nanometers + 13 => 0.001, // Microns + 14 => 100.0, // Decimeters + 15 => 10_000.0, // Decameters + 16 => 100_000.0, // Hectometers + 17 => 1.0e12, // Gigameters + 18 => 1.496e14, // Astronomical Units + 19 => 9.461e18, // Light Years + 20 => 3.086e19, // Parsecs + 21 => 304.800_609_6, // US Survey Feet + _ => 1.0, + } +} diff --git a/src/entities/common.rs b/src/entities/common.rs index 545d3d0b5..6e01a7267 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -1,7 +1,126 @@ +use std::cell::Cell; + use glam::Vec3; use crate::scene::object::{GripDef, GripShape, PropValue, Property}; +/// Linear / angular unit format pulled from the document header so the +/// per-thread properties pipeline can format values consistently without +/// passing the document through every callsite. +#[derive(Clone, Copy, Default)] +pub struct UnitContext { + /// LUNITS — 1=Sci, 2=Decimal, 3=Engineering, 4=Architectural, 5=Fractional + pub lunits: i16, + /// LUPREC — decimal places (linear) + pub luprec: i16, + /// AUNITS — 0=Decimal degrees, 1=DMS, 2=Grad, 3=Rad. Surfaced via + /// `format_angle`, which is read on demand by code that already + /// formats angular values via radians. + #[allow(dead_code)] + pub aunits: i16, + /// AUPREC — decimal places (angular) + #[allow(dead_code)] + pub auprec: i16, +} + +thread_local! { + static UNIT_CTX: Cell = const { Cell::new(UnitContext { + lunits: 2, + luprec: 4, + aunits: 0, + auprec: 0, + }) }; +} + +/// Set the per-thread unit context. Properties helpers consult it when +/// they format f64 values into display strings. +pub fn set_unit_context(ctx: UnitContext) { + UNIT_CTX.with(|c| c.set(ctx)); +} + +pub fn unit_context() -> UnitContext { + UNIT_CTX.with(|c| c.get()) +} + +/// Format a linear length using LUNITS / LUPREC. Architectural / fractional +/// produce "n'-d/D"" style strings (1 unit = 1 inch); decimal / scientific / +/// engineering / Windows-desktop fall back to plain decimal at LUPREC places. +pub fn format_length(value: f64) -> String { + let ctx = unit_context(); + let prec = ctx.luprec.max(0) as usize; + match ctx.lunits { + 1 => format!("{:.*e}", prec, value), + 3 => { + // Engineering: ft-inches, decimal inches. + let sign = if value < 0.0 { "-" } else { "" }; + let abs = value.abs(); + let feet = (abs / 12.0).trunc(); + let rem = abs - feet * 12.0; + format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem) + } + 4 | 5 => { + // Architectural / Fractional — n + fraction with 1/2^p denom (1 + // unit = 1 inch). Use 6 as a moderate denominator power so the + // result reads like 1/64". + let sign = if value < 0.0 { "-" } else { "" }; + let abs = value.abs(); + let (feet, in_rem) = if ctx.lunits == 4 { + let f = (abs / 12.0).trunc(); + (Some(f as i64), abs - f * 12.0) + } else { + (None, abs) + }; + let whole = in_rem.trunc(); + let frac = in_rem - whole; + let denom = 64u64; + let numer = (frac * denom as f64).round() as i64; + let mut n = numer as u64; + let mut d = denom; + while d > 1 && n % 2 == 0 && d % 2 == 0 { + n /= 2; + d /= 2; + } + let frac_str = if n == 0 || d == 1 { + String::new() + } else { + format!(" {}/{}", n, d) + }; + let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" }; + match feet { + Some(f) => format!("{}{}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix), + None => format!("{}{:.0}{}", sign, whole, frac_str), + } + } + _ => format!("{:.*}", prec, value), + } +} + +/// Format an angle (input in radians) using AUNITS / AUPREC. +#[allow(dead_code)] +pub fn format_angle(value_rad: f64) -> String { + let ctx = unit_context(); + let prec = ctx.auprec.max(0) as usize; + match ctx.aunits { + 1 => { + // DMS — degrees / minutes / seconds. + let deg = value_rad.to_degrees(); + let sign = if deg < 0.0 { "-" } else { "" }; + let a = deg.abs(); + let d = a.floor(); + let m_full = (a - d) * 60.0; + let m = m_full.floor(); + let s = (m_full - m) * 60.0; + format!("{}{:.0}°{:.0}'{:.*}\"", sign, d, m, prec, s) + } + 2 => { + let g = value_rad.to_degrees() / 0.9; + format!("{:.*}g", prec, g) + } + 3 => format!("{:.*}r", prec, value_rad), + _ => format!("{:.*}°", prec, value_rad.to_degrees()), + } +} + pub fn square_grip(id: usize, world: Vec3) -> GripDef { GripDef { id, @@ -33,7 +152,7 @@ pub fn edit_prop(label: &'static str, field: &'static str, value: f64) -> Proper Property { label: label.into(), field, - value: PropValue::EditText(format!("{value:.4}")), + value: PropValue::EditText(format_length(value)), } } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index d3e6fffa9..275208f24 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,7 +1,7 @@ pub mod arc; pub mod attribute; pub mod circle; -mod common; +pub mod common; pub mod dimension; pub mod ellipse; pub mod hatch; diff --git a/src/io/xref.rs b/src/io/xref.rs index cca3acab7..46135fbe1 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -143,6 +143,13 @@ fn merge_xref_into_block( ) { let prefix = xref_block_name; + // Carry the xref's INSUNITS onto the host BlockRecord. Used at INSERT + // time so the inserted xref scales to the host's units (INSUNITS). + let src_insunits = xref_doc.header.insertion_units; + if let Some(br) = doc.block_records.iter_mut().find(|b| b.handle == br_handle) { + br.units = src_insunits; + } + // ── Layers ────────────────────────────────────────────────────────── // Prefix every xref layer (including "0"). Entity layer references are // remapped below so the resolver finds the merged copy. Host layers