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
@@ -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(())@@ -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);@@ -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));+ }+}new file mode 100644@@ -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)));+}