collective

Browse / github.com/storytold/cadcraft / patch

Dimensions: a negative DIMGAP draws a frame around the dimension text

Dimensions: a negative DIMGAP draws a frame around the dimension text ## What was missing CADCraft used only the size of DIMGAP, so a negative value placed the text like a positive one and drew no frame. In AutoCAD, "If you enter a negative value, DIMGAP places a box around the dimension text" ([DIMGAP (System Variable)](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT-MAC/files/GUID-EB367187-A5D6-404A-8121-A6D1D25B626A.htm)), and Draw Frame Around Text on the Text tab of the Dimension Style Manager "changes the value stored in the DIMGAP system variable to a negative value" ([Text Tab (Dimension Style Manager)](https://help.autodesk.com/cloudhelp/2026/E

Status
open
Score
0.00
Adopters
0 other operators
Author
dotCooCoo (imported from pull request #2111, open)
Base
b90165666917ec3ac6c308c2ea1c55bb6ea8317d
Size
+243 −1 in 5 files
Review
pending
Shared
2026-10-28
Id
62925198d0ce4ae24503d1b7

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/tests/it/dim_text_frame.rs crates/engine/tests/it/main.rs crates/io/tests/it/dim_text_frame.rs crates/io/tests/it/main.rs crates/render/src/dim.rs

diff --git a/crates/engine/tests/it/dim_text_frame.rs b/crates/engine/tests/it/dim_text_frame.rsnew file mode 100644--- /dev/null+++ b/crates/engine/tests/it/dim_text_frame.rs@@ -0,0 +1,30 @@+//! A negative DIMGAP draws a frame around the dimension text: SETVAR takes the negative value, a+//! new dimension draws the frame, and EXPLODE turns it into four lines.++use cadcraft_engine::Session;+use cadcraft_engine::doc::{EntityKind, Handle};+use serde_json::json;++/// Draw a linear dimension with DIMGAP `gap`, explode it, and return the number of lines the+/// dimension drew and the number of LINEs EXPLODE left.+fn explode_with_gap(gap: f64) -> (usize, usize) {+    let mut s = Session::new();+    s.execute("setvar", &json!({ "name": "DIMGAP", "value": gap })).unwrap();+    let r = s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [30, 0], "at": [15, 5] })).unwrap();+    let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();+    let drawn = match &s.doc().unwrap().entity(h).unwrap().kind {+        EntityKind::Dimension(dm) => cadcraft_engine::render::dimension_in(s.doc().unwrap(), dm).lines.len(),+        k => panic!("not a dimension: {k:?}"),+    };+    s.execute("explode", &json!({ "handles": [h.hex()] })).unwrap();+    let lines = s.doc().unwrap().model.iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).count();+    (drawn, lines)+}++#[test]+fn negative_dimgap_frame_is_drawn_and_exploded() {+    let (plain_drawn, plain_lines) = explode_with_gap(0.09);+    let (drawn, lines) = explode_with_gap(-0.09);+    assert_eq!(drawn, plain_drawn + 4);+    assert_eq!(lines, plain_lines + 4);+}diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs--- a/crates/engine/tests/it/main.rs+++ b/crates/engine/tests/it/main.rs@@ -28,6 +28,7 @@ mod dim_explode_text; mod dim_properties; mod dim_radial_inside; mod dim_radial_outside;+mod dim_text_frame; mod dim_vars; mod dimtedit_prompts; mod drafting_spacing;diff --git a/crates/io/tests/it/dim_text_frame.rs b/crates/io/tests/it/dim_text_frame.rsnew file mode 100644--- /dev/null+++ b/crates/io/tests/it/dim_text_frame.rs@@ -0,0 +1,73 @@+//! A negative DIMGAP draws a frame around the dimension text. The `*D` block written for the+//! dimension holds the frame's four sides, the negative DIMGAP reads back from the file, and a+//! PDF plot draws the frame.++use cadcraft_doc::{Common, DimKind, Dimension, Drawing, EntityKind, Space};+use cadcraft_geom::Vec3;++use crate::pdf_read;++fn linear() -> Dimension {+    Dimension {+        kind: DimKind::Linear { rotation: 0.0 },+        defpt: Vec3::new(15.0, 5.0, 0.0),+        text_mid: Vec3::ZERO,+        p13: Vec3::ZERO,+        p14: Vec3::new(30.0, 0.0, 0.0),+        p15: Vec3::ZERO,+        p16: Vec3::ZERO,+        text: String::new(),+        style: "Standard".into(),+        measurement: 0.0,+        text_rotation: 0.0,+        user_text_pos: false,+        block: None,+        overrides: Default::default(),+        assoc: Vec::new(),+        marks: Default::default(),+    }+}++/// Save a drawing with one dimension in a Standard style of DIMGAP `gap`, read it back, and+/// return the style's DIMGAP and the number of LINEs in the dimension's `*D` block.+fn roundtrip(gap: f64) -> (f64, usize) {+    let mut d = Drawing::new_imperial();+    d.dim_styles.iter_mut().for_each(|s| s.text_gap = gap);+    d.add(&Space::Model, Common::default(), EntityKind::Dimension(linear())).unwrap();+    let back = cadcraft_io::read_dxf(cadcraft_io::write_dxf(&d).as_bytes()).unwrap();+    let dm = back+        .model+        .iter()+        .find_map(|e| match &e.kind {+            EntityKind::Dimension(dm) => Some(dm.clone()),+            _ => None,+        })+        .unwrap();+    let block = back.block(dm.block.as_deref().unwrap()).unwrap();+    let lines = block.entities.iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).count();+    (back.dim_style("Standard").unwrap().text_gap, lines)+}++/// The number of path segments (`l`) in the PDF of a drawing with one dimension in a Standard+/// style of DIMGAP `gap`.+fn pdf_segments(gap: f64) -> usize {+    let mut d = Drawing::new_imperial();+    d.dim_styles.iter_mut().for_each(|s| s.text_gap = gap);+    d.add(&Space::Model, Common::default(), EntityKind::Dimension(linear())).unwrap();+    let doc = pdf_read::read(&cadcraft_io::plot(&d, &Space::Model, &serde_json::json!({"compress": false})).unwrap());+    pdf_read::count(&doc.pages[0].content, "l")+}++#[test]+fn pdf_draws_the_text_frame() {+    assert_eq!(pdf_segments(-0.09), pdf_segments(0.09) + 4);+}++#[test]+fn dimension_block_holds_the_text_frame() {+    let (gap, plain) = roundtrip(0.09);+    assert!((gap - 0.09).abs() < 1e-12, "{gap}");+    let (gap, framed) = roundtrip(-0.09);+    assert!((gap + 0.09).abs() < 1e-12, "{gap}");+    assert_eq!(framed, plain + 4);+}diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs--- a/crates/io/tests/it/main.rs+++ b/crates/io/tests/it/main.rs@@ -12,6 +12,7 @@ mod codepages; mod dim_block_resave_roundtrip; mod dim_block_text; mod dim_marks_roundtrip;+mod dim_text_frame; mod dimstyle_vars_roundtrip; mod drafting_spacing; #[cfg(not(target_arch = "wasm32"))]diff --git a/crates/render/src/dim.rs b/crates/render/src/dim.rs--- a/crates/render/src/dim.rs+++ b/crates/render/src/dim.rs@@ -555,6 +555,8 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font     };     let mut text_angle = 0.0;     let mut text_mid = d.text_mid.xy();+    // DIMARCSYM 1: the arc symbol's half width and how far its top is above the text's middle.+    let mut arc_over: Option<(f64, f64)> = None;     // Place an end marker: tick or arrowhead. Returns how far the dimension line stops short.     let end = |g: &mut DimGeometry, kind: Arrowhead, tip: Vec2, dir: Vec2| -> f64 {         if ticks {@@ -978,9 +980,12 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font             if st.arc_symbol == 1 {                 // A small arc over the text.                 let (tw, tht) = measure(&g.mtext);-                let (x, y) = (Vec2::from_angle(text_angle), Vec2::from_angle(text_angle).perp());+                // The symbol turns with the text, a DIMTEDIT Angle included.+                let a = if d.text_rotation != 0.0 && d.text_rotation.is_finite() { d.text_rotation } else { text_angle };+                let (x, y) = (Vec2::from_angle(a), Vec2::from_angle(a).perp());                 let half = (tw / 2.0).max(th / 2.0);                 let base = text_mid + y * (tht / 2.0 + gap / 2.0);+                arc_over = Some((half, tht / 2.0 + gap / 2.0 + th * 0.35));                 let sym: Vec<Vec2> = (0..=12)                     .map(|i| {                         let t = f64::from(i) / 6.0 - 1.0;@@ -1045,6 +1050,19 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font             g.text_glyphs.extend(p.shaped.glyphs);         }     }+    // DIMGAP below 0: a frame around the text and the arc symbol over it, widened by |DIMGAP| on+    // every side. A DIMINSPECT frame takes its place.+    if st.text_gap < 0.0 && !g.mtext.is_empty() && d.marks.inspect.as_ref().is_none_or(|ins| ins.shape >= 2) {+        let (w, h) = measure(&g.mtext);+        let (x, y) = (Vec2::from_angle(text_angle), Vec2::from_angle(text_angle).perp());+        let (hw, top) = arc_over.map_or((w / 2.0, h / 2.0), |(half, peak)| ((w / 2.0).max(half), (h / 2.0).max(peak)));+        let (hw, top, bottom) = (hw + gap, top + gap, h / 2.0 + gap);+        let corners: Vec<Vec2> =+            [(-hw, -bottom), (hw, -bottom), (hw, top), (-hw, top)].into_iter().map(|(px, py)| text_mid + x * px + y * py).collect();+        for (a, b) in corners.iter().zip(corners.iter().cycle().skip(1)) {+            g.dim(vec![*a, *b]);+        }+    }     g.text_pos = text_mid;     g.text_angle = text_angle;     g@@ -1212,6 +1230,125 @@ mod tests {         assert!(outside2.text_pos.y < 5.0);     } +    /// The last four lines of `g` as a frame: four sides, each starting where the one before ends.+    fn frame_of(g: &DimGeometry) -> Vec<Vec<Vec2>> {+        let sides: Vec<Vec<Vec2>> = g.lines.iter().rev().take(4).rev().cloned().collect();+        assert_eq!(sides.len(), 4);+        for (a, b) in sides.iter().zip(sides.iter().cycle().skip(1)) {+            assert_eq!(a.len(), 2);+            assert!(a[1].dist(b[0]) < 1e-9, "{sides:?}");+        }+        sides+    }++    #[test]+    fn negative_dimgap_draws_a_frame_around_the_text() {+        let d = lin((0.0, 0.0), (10.0, 0.0), (0.0, 5.0), 0.0);+        let plain = dimension_geometry(&d, &st_with(|s| s.text_gap = 0.09), 1.0);+        let framed = dimension_geometry(&d, &st_with(|s| s.text_gap = -0.09), 1.0);+        // The same text place and dimension line break as DIMGAP 0.09, and four more lines.+        assert!(framed.text_pos.dist(plain.text_pos) < 1e-12);+        assert_eq!(framed.lines.len(), plain.lines.len() + 4);+        assert_eq!(framed.lines_of(LineRole::Dim).count(), plain.lines_of(LineRole::Dim).count() + 4);+        // The frame is the text box widened by 0.09 on every side, around the text.+        let frame = bbox(&frame_of(&framed));+        let l = cadcraft_fonts::layout_mtext_with(&framed.mtext, &MTextParams { attach: 5, ..MTextParams::new(Vec2::ZERO, 0.18) });+        assert!((frame.width() - (l.bounds.width() + 0.18)).abs() < 1e-9, "{frame:?}");+        assert!((frame.height() - (l.bounds.height() + 0.18)).abs() < 1e-9, "{frame:?}");+        let text = bbox(&framed.text);+        assert!(frame.min.x < text.min.x && text.max.x < frame.max.x && frame.min.y < text.min.y && text.max.y < frame.max.y);+        assert!((frame.center().dist(framed.text_pos)) < 1e-9, "{frame:?}");+        // DIMGAP 0 and above: no frame.+        assert_eq!(dimension_geometry(&d, &st_with(|s| s.text_gap = 0.0), 1.0).lines.len(), plain.lines.len());+    }++    #[test]+    fn the_frame_turns_with_the_text() {+        let d = lin((0.0, 0.0), (0.0, 10.0), (5.0, 0.0), std::f64::consts::FRAC_PI_2);+        let g = dimension_geometry(+            &d,+            &st_with(|s| {+                s.text_gap = -0.09;+                s.text_inside_horizontal = false;+            }),+            1.0,+        );+        assert!((g.text_angle.abs() - std::f64::consts::FRAC_PI_2).abs() < 1e-9, "{}", g.text_angle);+        // Two sides along the text, two across it, the long ones along the dimension line.+        for s in frame_of(&g) {+            let v = s[1] - s[0];+            assert!(v.x.abs() < 1e-9 || v.y.abs() < 1e-9, "{v:?}");+        }+        let frame = bbox(&frame_of(&g));+        assert!(frame.height() > frame.width(), "{frame:?}");+        assert!(bbox(&g.text).max.y < frame.max.y && frame.min.y < bbox(&g.text).min.y);+    }++    #[test]+    fn the_frame_takes_in_the_arc_symbol_over_the_text() {+        let mut d = lin((10.0, 0.0), (0.0, 10.0), (12.0, 0.0), 0.0);+        d.kind = DimKind::ArcLength;+        let g = dimension_geometry(+            &d,+            &st_with(|s| {+                s.text_gap = -0.09;+                s.arc_symbol = 1;+                s.text_inside_horizontal = true;+            }),+            1.0,+        );+        let frame = bbox(&frame_of(&g));+        // The arc symbol is the line drawn just before the frame.+        let sym = &g.lines[g.lines.len() - 5];+        assert_eq!(sym.len(), 13);+        let s = bbox(std::slice::from_ref(sym));+        assert!((frame.max.y - (s.max.y + 0.09)).abs() < 1e-9, "{frame:?} {s:?}");+        assert!(frame.min.x < s.min.x && s.max.x < frame.max.x, "{frame:?} {s:?}");+        let text = bbox(&g.text);+        assert!(frame.min.y < text.min.y && text.max.y < frame.max.y, "{frame:?} {text:?}");+    }++    #[test]+    fn the_arc_symbol_and_the_frame_turn_with_a_text_angle() {+        let mut d = lin((10.0, 0.0), (0.0, 10.0), (12.0, 0.0), 0.0);+        d.kind = DimKind::ArcLength;+        d.text_rotation = 0.5;+        let g = dimension_geometry(+            &d,+            &st_with(|s| {+                s.text_gap = -0.09;+                s.arc_symbol = 1;+            }),+            1.0,+        );+        assert!((g.text_angle - 0.5).abs() < 1e-12);+        // In the text's own axes, the frame is upright and holds the symbol 0.09 below its top.+        let (x, y) = (Vec2::from_angle(0.5), Vec2::from_angle(0.5).perp());+        let local = |pts: &[Vec<Vec2>]| -> Vec<Vec<Vec2>> {+            pts.iter().map(|l| l.iter().map(|p| Vec2::new((*p - g.text_pos).dot(x), (*p - g.text_pos).dot(y))).collect()).collect()+        };+        let frame = bbox(&local(&frame_of(&g)));+        let sym = bbox(&local(std::slice::from_ref(&g.lines[g.lines.len() - 5])));+        assert!((frame.max.y - (sym.max.y + 0.09)).abs() < 1e-9, "{frame:?} {sym:?}");+        assert!(frame.min.x < sym.min.x && sym.max.x < frame.max.x, "{frame:?} {sym:?}");+        // The symbol's ends sit level in the text's axes.+        let s = &local(std::slice::from_ref(&g.lines[g.lines.len() - 5]))[0];+        assert!((s[0].y - s[12].y).abs() < 1e-9, "{s:?}");+    }++    #[test]+    fn an_inspection_frame_takes_the_place_of_the_dimgap_frame() {+        let mut d = lin((0.0, 0.0), (10.0, 0.0), (0.0, 5.0), 0.0);+        d.marks.inspect = Some(cadcraft_doc::Inspection { shape: 0, label: "A".into(), rate: "50%".into() });+        let st = |gap: f64| st_with(|s| s.text_gap = gap);+        let inspected = dimension_geometry(&d, &st(0.09), 1.0).lines.len();+        assert_eq!(dimension_geometry(&d, &st(-0.09), 1.0).lines.len(), inspected);+        // Shape none draws no inspection frame, so DIMGAP's frame is drawn.+        d.marks.inspect = Some(cadcraft_doc::Inspection { shape: 2, label: String::new(), rate: String::new() });+        let none = dimension_geometry(&d, &st(0.09), 1.0).lines.len();+        assert_eq!(dimension_geometry(&d, &st(-0.09), 1.0).lines.len(), none + 4);+    }+     #[test]     fn dimtad_one_uses_autocad_text_clearance() {         let d = lin((0.0, 0.10), (0.0, 0.70), (0.30, 0.40), std::f64::consts::FRAC_PI_2);