collective

Browse / github.com/storytold/cadcraft / patch

INSERT {file}: convert the file's $INSUNITS to the drawing's units; WBLOCK keeps the source units (#1997)

INSERT {file} ignores $INSUNITS: a block drawn in inches comes into a drawing in metres at scale 1 (36 × 29 m instead of 0.9144 × 0.7366 m), where AutoCAD inserts it at 0.0254 **Environment:** CADCraft 0.12.0 (release MSI, `cadcraft-cli 0.12.0`), Windows 11 Pro. Interop observed in Autodesk AutoCAD 2026 (`accoreconsole`, opening a copy, never saving over it) — black-box observation only. All drawings are synthetic and generated by CADCraft; no AutoCAD-produced file is attached. Reproduced on v0.12.0: the three files below are byte-identical to the v0.11.0 ones on which the AutoCAD observation was made. main at `1977a49` reads the same in the lines quoted under Hint. Goes together with #2002 (WBLOCK always writes `$INSUNITS = 1`; split out of #1996): both fixes go t

Status
open
Score
0.00
Adopters
0 other operators
Author
MarekSpodBiedry (imported from pull request #2006, open)
Base
f043540d13dac03ce895a96c78a8349f3e40bd5b
Size
+119 −7 in 4 files
Review
pending
Shared
2026-10-28
Id
0fa955f7665b19357f8759a7

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/blocks.rs crates/engine/src/cmd/blocks/insert_file.rs crates/engine/tests/insert_file.rs crates/engine/tests/wblock_units.rs

diff --git a/crates/engine/src/cmd/blocks.rs b/crates/engine/src/cmd/blocks.rs--- a/crates/engine/src/cmd/blocks.rs+++ b/crates/engine/src/cmd/blocks.rs@@ -36,7 +36,7 @@ pub fn specs() -> Vec<CommandSpec> {         CommandSpec::new("insert", "Block...", run_insert)             .menu(&["Insert", "Block..."])             .alias(&["i", "-insert", "ddinsert"])-            .params("{name (\"*NAME\": exploded), file?: DXF/DWG path (defines or redefines block `name`, default the file name, from its model space; base point $INSBASE), at: [x,y], scale?: number | [x, y], rotation? (degrees), basePoint?: [x,y] (block coordinates), attribs?: {TAG: value}, explode?: bool}")+            .params("{name (\"*NAME\": exploded), file?: DXF/DWG path (defines or redefines block `name`, default the file name, from its model space; base point $INSBASE; its $INSUNITS converted to this drawing's on top of `scale`), at: [x,y], scale?: number | [x, y], rotation? (degrees), basePoint?: [x,y] (block coordinates), attribs?: {TAG: value}, explode?: bool}")             .interactive(|_| Ok(Box::new(InsertM::default()))),         CommandSpec::new("attdef", "Define Attributes...", run_attdef)             .menu(&["Draw", "Block", "Define Attributes..."])@@ -341,6 +341,8 @@ fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> {     d.text_styles = src.text_styles.clone();     d.dim_styles = src.dim_styles.clone();     d.blocks = src.blocks.clone();+    // The file keeps the units of the drawing it came from, so INSERT can convert them back (#1996).+    d.header.set_i64("INSUNITS", src.header.i64("INSUNITS", 1));     // The base point becomes the written drawing's origin, its insertion base: by default 0,0 for     // objects and the block's base point for a block (`base` is then in block coordinates).     let base = point_param(p, "base");@@ -579,6 +581,8 @@ struct InsertM {     explode: bool,     /// The file waiting for the Redefine answer.     file: Option<insert_file::FileRef>,+    /// A file's units converted to this drawing's, applied to the scale (None: not from a file).+    unit_factor: Option<f64>, }  impl InsertM {@@ -595,7 +599,7 @@ impl InsertM {             && self.values.len() >= self.attdefs.len()             && let (Some(n), Some(a), Some(sc), Some(r)) = (self.name.clone(), self.at, self.scale, self.rotation)         {-            let h = insert_with(s, &n, a, sc, r, self.base, &self.values)?;+            let h = insert_with(s, &n, a, sc * self.unit_factor.unwrap_or(1.0), r, self.base, &self.values)?;             if self.explode {                 insert_file::explode_insert(s, h)?;             }@@ -606,6 +610,7 @@ impl InsertM {      /// Block `name` chosen: collect its attribute prompts and ask for the insertion point.     fn choose(&mut self, s: &mut Session, name: &str) -> Result<()> {+        self.unit_factor = None;         let Some(b) = s.doc()?.block(name).cloned() else {             s.echo(format!("Block \"{name}\" not found."));             return Ok(());@@ -633,7 +638,11 @@ impl InsertM {     /// Define the block from a file, then choose it; a file that can't be read is reported.     fn choose_file(&mut self, s: &mut Session, f: &insert_file::FileRef) -> Result<()> {         match f.define(s) {-            Ok(name) => self.choose(s, &name),+            Ok((name, factor)) => {+                self.choose(s, &name)?;+                self.unit_factor = Some(factor);+                Ok(())+            }             Err(e) => {                 s.echo(e.to_string());                 Ok(())diff --git a/crates/engine/src/cmd/blocks/insert_file.rs b/crates/engine/src/cmd/blocks/insert_file.rs--- a/crates/engine/src/cmd/blocks/insert_file.rs+++ b/crates/engine/src/cmd/blocks/insert_file.rs@@ -34,10 +34,52 @@ impl FileRef {         !self.explicit && s.doc().is_ok_and(|d| d.block(&self.name).is_some())     } -    /// Read the file and (re)define its block. Returns the block's name.-    pub fn define(&self, s: &mut Session) -> Result<String> {+    /// Read the file and (re)define its block. Returns the block's name and the factor that+    /// converts the file's `$INSUNITS` to this drawing's (1 when either declares no units).+    pub fn define(&self, s: &mut Session) -> Result<(String, f64)> {         let src = read_drawing(&self.path)?;-        define_from(s, &src, &self.name)+        let name = define_from(s, &src, &self.name)?;+        Ok((name, unit_factor(&src, s.doc()?)))+    }+}++/// Metres in one `$INSUNITS` unit (DXF Reference); `None` for 0 (unitless) and unknown codes.+fn unit_metres(code: i64) -> Option<f64> {+    Some(match code {+        1 => 0.0254,+        2 => 0.3048,+        3 => 1609.344,+        4 => 0.001,+        5 => 0.01,+        6 => 1.0,+        7 => 1000.0,+        8 => 2.54e-8,+        9 => 2.54e-5,+        10 => 0.9144,+        11 => 1e-10,+        12 => 1e-9,+        13 => 1e-6,+        14 => 0.1,+        15 => 10.0,+        16 => 100.0,+        17 => 1e9,+        18 => 1.495_978_707e11,+        19 => 9.460_730_472_580_8e15,+        20 => 3.085_677_581_491_4e16,+        21 => 1200.0 / 3937.0,+        22 => 1.0 / 39.37,+        23 => 3600.0 / 3937.0,+        24 => 6_336_000.0 / 3937.0,+        _ => return None,+    })+}++/// The scale that brings a file drawn in `src`'s units into `target`'s, as AutoCAD does (1 inch+/// file into a metre drawing: 0.0254). A drawing without units is taken as is.+fn unit_factor(src: &Drawing, target: &Drawing) -> f64 {+    match (unit_metres(src.header.i64("INSUNITS", 0)), unit_metres(target.header.i64("INSUNITS", 0))) {+        (Some(from), Some(to)) => from / to,+        _ => 1.0,     } } @@ -230,7 +272,15 @@ pub(super) fn json_params(s: &mut Session, p: &Value) -> Result<Value> {     }     if let Some(path) = str_param(&p, "file").map(str::to_string) {         let n = name.clone().filter(|n| !n.is_empty()).unwrap_or_else(|| stem(&path));-        name = Some(FileRef { name: n, path, explicit: true }.define(s)?);+        let (defined, factor) = FileRef { name: n, path, explicit: true }.define(s)?;+        name = Some(defined);+        if factor != 1.0 {+            // The file's units to this drawing's, on top of the scale asked for.+            p["scale"] = match p.get("scale") {+                Some(Value::Array(a)) => Value::Array(a.iter().map(|v| v.as_f64().map_or_else(|| v.clone(), |x| json!(x * factor))).collect()),+                other => json!(other.and_then(Value::as_f64).unwrap_or(1.0) * factor),+            };+        }     }     if let Some(n) = name {         p["name"] = json!(n);diff --git a/crates/engine/tests/insert_file.rs b/crates/engine/tests/insert_file.rs--- a/crates/engine/tests/insert_file.rs+++ b/crates/engine/tests/insert_file.rs@@ -174,3 +174,31 @@ fn exploded_json_insert_is_one_undo_step() {         assert_eq!(lines(&s), vec![(0.0, 0.0, 10.0, 0.0)], "{params}");     } }++/// #1997: a file drawn in inches comes into a drawing in metres at 0.0254, on top of the scale asked for.+#[test]+fn a_file_in_other_units_is_scaled_to_this_drawings_units() {+    hooks();+    let part = file("inches.dxf", "0,0");+    let scale_of = |s: &Session| {+        let d = s.doc().unwrap();+        d.model.iter().rev().find_map(|e| if let EntityKind::Insert(i) = &e.kind { Some((i.scale.x, i.scale.y)) } else { None }).unwrap()+    };+    let near = |a: (f64, f64), b: (f64, f64)| (a.0 - b.0).abs() < 1e-12 && (a.1 - b.1).abs() < 1e-12;+    let mut s = Session::new();+    s.execute("units", &json!({ "insunits": 6 })).unwrap();+    s.execute("insert", &json!({ "file": part, "at": [0, 0] })).unwrap();+    assert!(near(scale_of(&s), (0.0254, 0.0254)), "{:?}", scale_of(&s));+    s.execute("insert", &json!({ "file": part, "at": [0, 0], "scale": [2, 3] })).unwrap();+    assert!(near(scale_of(&s), (0.0508, 0.0762)), "{:?}", scale_of(&s));+    // Interactive INSERT converts too.+    s.script(&format!("-INSERT inches={part} 0,0 1 1 0\n")).unwrap();+    assert!(near(scale_of(&s), (0.0254, 0.0254)), "{:?}", scale_of(&s));+    // A drawing in the same units, or without any, takes the file as is.+    for units in [1, 0] {+        let mut s = Session::new();+        s.execute("units", &json!({ "insunits": units })).unwrap();+        s.execute("insert", &json!({ "file": part, "at": [0, 0] })).unwrap();+        assert!(near(scale_of(&s), (1.0, 1.0)), "INSUNITS {units}: {:?}", scale_of(&s));+    }+}diff --git a/crates/engine/tests/wblock_units.rs b/crates/engine/tests/wblock_units.rsnew file mode 100644--- /dev/null+++ b/crates/engine/tests/wblock_units.rs@@ -0,0 +1,25 @@+//! WBLOCK writes the drawing's $INSUNITS, so INSERT of the written file, which converts the file's+//! units to the drawing's (#1997), brings it back at its size (#1996).++use cadcraft_engine::Session;+use cadcraft_engine::cmd::file::{IoHooks, set_io};+use cadcraft_engine::doc::EntityKind;+use serde_json::json;++#[test]+fn a_wblock_file_from_a_metric_drawing_inserts_back_at_its_size() {+    set_io(IoHooks {+        read: |b, name| cadcraft_io::read(b, name).map_err(|e| e.to_string()),+        write: |d, name| cadcraft_io::write(d, name).map_err(|e| e.to_string()),+        plot: None,+    });+    let path = std::env::temp_dir().join(format!("cadcraft-wblock-units-{}.dxf", std::process::id())).to_string_lossy().into_owned();+    let mut s = Session::new();+    s.execute("units", &json!({ "insunits": 6 })).unwrap();+    let h = s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap()["handles"][0].as_str().unwrap().to_string();+    s.execute("wblock", &json!({ "handles": [h], "path": &path })).unwrap();+    s.execute("insert", &json!({ "name": "PART", "file": &path, "at": [0, 0] })).unwrap();+    let _ = std::fs::remove_file(&path);+    let scale = s.doc().unwrap().model.iter().find_map(|e| if let EntityKind::Insert(i) = &e.kind { Some((i.scale.x, i.scale.y)) } else { None });+    assert_eq!(scale, Some((1.0, 1.0)));+}