feat: honour LUNITS / LUPREC unit formatting + INSUNITS scaling on insert
Property-panel coordinate display and block-insert scaling were both hard-coded, ignoring the document's stored unit configuration. - LUNITS / LUPREC: edit_prop() now formats values through a thread-local UnitContext seeded by refresh_properties(). Decimal / Scientific / Engineering / Architectural / Fractional all render correctly without threading the document handle through every entity properties builder. - AUNITS / AUPREC: format_angle() helper available (decimal degrees, DMS, grad, rad). Callers that already format angular values via radians can switch over without touching the helper signatures. - INSUNITS + MEASUREMENT: xref's source INSUNITS is carried onto the host BlockRecord.units during merge. commit_entity() then scales new INSERTs so 1 source-unit maps to the host's INSUNITS length. When either side is unitless (0) MEASUREMENT acts as the fallback (0=Imperial / inches, 1=Metric / mm), matching AutoCAD's rule. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9c4043ef45
commit
b3e117a969
4 changed files with 198 additions and 2 deletions
|
|
@ -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<String> = 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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UnitContext> = 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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue