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