From 79efd97515d56b8130993ac7b58258b5b7290929 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:10:24 +0200 Subject: [PATCH] MTEXT: annotative scaling and SCALE on multileader text scale the text width and columns too (fixes #708, fixes #709) (#743) Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/doc/src/anno.rs | 6 ++++ crates/doc/src/entity.rs | 4 +++ crates/engine/tests/annotative.rs | 53 ++++++++++++++++++++++++++++ crates/engine/tests/mtext_columns.rs | 48 ++++++++++++++++++++++++- 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/anno.rs b/crates/doc/src/anno.rs index e1a6fdf..6662136 100644 --- a/crates/doc/src/anno.rs +++ b/crates/doc/src/anno.rs @@ -159,6 +159,9 @@ pub fn anno_scaled(e: &Entity, f: f64) -> Entity { EntityKind::MText(t) => { t.height *= f; t.width *= f; + if let Some(c) = &mut t.columns { + c.scale(f); + } } EntityKind::Dimension(dm) => { dm.overrides.insert("scale".into(), serde_json::json!(f)); @@ -172,6 +175,9 @@ pub fn anno_scaled(e: &Entity, f: f64) -> Entity { t.insert = land + (t.insert - land) * f; t.height *= f; t.width *= f; + if let Some(c) = &mut t.columns { + c.scale(f); + } } if let Some(ins) = &mut m.block { ins.insert = land + (ins.insert - land) * f; diff --git a/crates/doc/src/entity.rs b/crates/doc/src/entity.rs index 2019963..5a8f30a 100644 --- a/crates/doc/src/entity.rs +++ b/crates/doc/src/entity.rs @@ -1212,6 +1212,10 @@ impl EntityKind { if let Some(t) = &mut ml.text { t3(&mut t.insert); t.height *= s; + t.width *= s; + if let Some(c) = &mut t.columns { + c.scale(s); + } } if let Some(b) = &mut ml.block { // Transformed exactly like a block reference. diff --git a/crates/engine/tests/annotative.rs b/crates/engine/tests/annotative.rs index 9c9d2ef..6a49ae9 100644 --- a/crates/engine/tests/annotative.rs +++ b/crates/engine/tests/annotative.rs @@ -283,3 +283,56 @@ fn hostile_parameters_are_refused() { assert!(s.execute("scalelistedit", &p).is_err(), "{p}"); } } + +#[test] +fn annotative_mtext_columns_follow_the_scale() { + // #708: at 1:2, two columns 10 wide with a gutter of 1 and a height of 20 on paper draw 20 + // wide, 2 apart and 40 tall: the box is 42 × 40, not 41 × 20. + let mut s = Session::new(); + setvar(&mut s, "CANNOSCALE", json!("1:2")); + let r = s + .execute( + "mtext", + &json!({ "at": [0, 0], "height": 1, "width": 10, "text": "a\\Pb\\Pc", "columns": { "type": "static", "count": 2, "gutter": 1, "height": 20 } }), + ) + .unwrap(); + let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap(); + s.execute("properties.set", &json!({ "handles": [h.hex()], "annotative": true })).unwrap(); + assert_eq!(scales(&s, h), ["1:2"]); + let v = |s: &mut Session, name: &str| s.execute("getvar", &json!({ "name": name })).unwrap()["value"].clone(); + assert_eq!(v(&mut s, "EXTMIN"), json!([0.0, -40.0, 0.0])); + assert_eq!(v(&mut s, "EXTMAX"), json!([42.0, 0.0, 0.0])); + // Dynamic columns with manual heights, and the text of a multileader, scale the same way. + let mut e = entity(&s, h); + if let EntityKind::MText(t) = &mut e.kind + && let Some(c) = &mut t.columns + { + c.heights = vec![3.0, 5.0]; + } + let mtext = match &e.kind { + EntityKind::MText(t) => t.clone(), + k => panic!("{k:?}"), + }; + let ml = Entity { + kind: EntityKind::MLeader(cadcraft_engine::doc::MLeader { + leaders: Vec::new(), + landing: Default::default(), + dogleg: 1.0, + text: Some(mtext), + style: "Standard".into(), + arrow_size: 1.0, + spline: false, + block: None, + }), + ..e.clone() + }; + for e in [e, ml] { + let t = match cadcraft_engine::doc::anno_scaled(&e, 2.0).kind { + EntityKind::MText(t) => t, + EntityKind::MLeader(m) => m.text.unwrap(), + k => panic!("{k:?}"), + }; + let c = t.columns.unwrap(); + assert_eq!((t.height, t.width, c.count, c.gutter, c.height, c.heights), (2.0, 20.0, 2, 2.0, 40.0, vec![6.0, 10.0])); + } +} diff --git a/crates/engine/tests/mtext_columns.rs b/crates/engine/tests/mtext_columns.rs index 84fde9a..38949e0 100644 --- a/crates/engine/tests/mtext_columns.rs +++ b/crates/engine/tests/mtext_columns.rs @@ -1,7 +1,7 @@ //! MTEXT columns and paragraph formatting from the command line, scripts and the JSON form //! (#532). -use cadcraft_doc::{EntityKind, MText, MTextColumnType}; +use cadcraft_doc::{EntityKind, MLeader, MText, MTextColumnType, MTextColumns}; use cadcraft_engine::Session; use serde_json::json; @@ -79,3 +79,49 @@ fn columns_shape_the_drawn_text_and_its_extents() { let b = cadcraft_doc::entity_bounds(d, e, 0); assert!((b.width() - 34.0).abs() < 1e-9 && (b.height() - 6.0).abs() < 1e-9, "{b:?}"); } + +#[test] +fn scale_scales_multileader_text_width_and_columns() { + // #709: SCALE 2 on a multileader's text doubles its width and column lengths like it does + // for a standalone MTEXT, not only its height. + let text = MText { + insert: Default::default(), + height: 2.0, + width: 10.0, + attach: 1, + rotation: 0.0, + style: "Standard".into(), + contents: "hello world".into(), + line_spacing: 1.0, + line_spacing_exact: false, + columns: Some(MTextColumns { + kind: MTextColumnType::Static, + count: 2, + gutter: 1.0, + height: 5.0, + auto_height: true, + heights: vec![3.0, 4.0], + flow_reversed: false, + }), + }; + let mut s = Session::new(); + let ml = MLeader { + leaders: Vec::new(), + landing: Default::default(), + dogleg: 1.0, + text: Some(text), + style: "Standard".into(), + arrow_size: 1.0, + spline: false, + block: None, + }; + let h = s.add_entity(EntityKind::MLeader(ml)).unwrap(); + s.execute("scale", &json!({ "handles": [h.hex()], "base": [0, 0], "factor": 2 })).unwrap(); + let t = match &s.doc().unwrap().entity(h).unwrap().kind { + EntityKind::MLeader(m) => m.text.clone().unwrap(), + k => panic!("{k:?}"), + }; + let c = t.columns.unwrap(); + assert_eq!((t.height, t.width), (4.0, 20.0)); + assert_eq!((c.kind, c.count, c.gutter, c.height, c.heights), (MTextColumnType::Static, 2, 2.0, 10.0, vec![6.0, 8.0])); +}