diff --git a/crates/engine/src/cmd/annotate.rs b/crates/engine/src/cmd/annotate.rs index 48254955a..7fa795377 100644 --- a/crates/engine/src/cmd/annotate.rs +++ b/crates/engine/src/cmd/annotate.rs @@ -260,18 +260,16 @@ fn radial(s: &mut Session, p: &Value, diameter: bool) -> Result { dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "") }; let k = match at { - Some(at) => text_inside(k, c, c.dist(pt), at), + Some(at) => text_at(k, at), None => k, }; Ok(json!({ "handle": add_dim(s, k)?.hex() })) } -/// A radius/diameter dimension line location picked inside the circle puts the text there, on -/// the dimension line (outside, the text keeps its default place beyond the arrowhead). -fn text_inside(mut k: EntityKind, c: Vec2, r: f64, at: Vec2) -> EntityKind { - if let EntityKind::Dimension(d) = &mut k - && at.dist(c) < r - { +/// The dimension line location of a radius/diameter dimension puts the text there, inside or +/// outside the circle, as a user-positioned text (DXF flag 70 bit 128), like AutoCAD. +fn text_at(mut k: EntityKind, at: Vec2) -> EntityKind { + if let EntityKind::Dimension(d) = &mut k { d.text_mid = v3(at); d.user_text_pos = true; } @@ -1106,7 +1104,7 @@ impl RadialM { } else { dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "") }); - text_inside(k, c, r, at) + text_at(k, at) } } diff --git a/crates/engine/tests/dim_radial_inside.rs b/crates/engine/tests/dim_radial_inside.rs index d16dac03a..7c1a9d17a 100644 --- a/crates/engine/tests/dim_radial_inside.rs +++ b/crates/engine/tests/dim_radial_inside.rs @@ -36,11 +36,10 @@ fn radial_text_goes_inside_when_the_location_is_inside() { assert!(covers(&g, 9.0) && !covers(&g, 4.0) && !covers(&g, 1.0)); assert!(g.lines_of(LineRole::Dim).flatten().all(|p| p.len() <= 10.0 + 1e-9)); - // Outside: the default placement beyond the arrowhead. + // Outside: the text goes to the point too (dim_radial_outside.rs), beyond the arrowhead. s.script("dimradius 0,10 0,15\n").unwrap(); assert!(s.running.is_none()); - let (dm, g) = last_dim(&s); - assert!(!dm.user_text_pos); + let (_, g) = last_dim(&s); assert!(g.text_pos.y > 10.0, "{:?}", g.text_pos); // JSON form, diameter: the line spans the circle, broken around the text. diff --git a/crates/engine/tests/dim_radial_outside.rs b/crates/engine/tests/dim_radial_outside.rs new file mode 100644 index 000000000..c01cc21a3 --- /dev/null +++ b/crates/engine/tests/dim_radial_outside.rs @@ -0,0 +1,62 @@ +//! DIMRADIUS / DIMDIAMETER: a dimension line location picked outside the circle puts the text +//! at that point (group 11, flag 70 bit 128), as for a location inside; the leader stops short +//! of the text instead of running into it. + +use cadcraft_engine::Session; +use cadcraft_engine::doc::{Dimension, EntityKind}; +use cadcraft_engine::geom::Vec2; +use cadcraft_engine::render::{DimGeometry, LineRole}; +use serde_json::json; + +fn last_dim(s: &Session) -> (Dimension, DimGeometry) { + let d = s.doc().unwrap(); + match &d.model.last().unwrap().kind { + EntityKind::Dimension(dm) => (dm.clone(), cadcraft_engine::render::dimension_in(d, dm)), + other => panic!("not a dimension: {other:?}"), + } +} + +/// Group 70 of every DIMENSION in the saved DXF. +fn dxf_flags(s: &Session) -> Vec { + let text = cadcraft_io::write_dxf(s.doc().unwrap()); + let lines: Vec<&str> = text.lines().map(str::trim).collect(); + let mut flags = Vec::new(); + let mut in_dim = false; + for p in lines.chunks(2) { + match p { + ["0", v] => in_dim = *v == "DIMENSION", + ["70", v] if in_dim => flags.push(v.parse().unwrap()), + _ => {} + } + } + flags +} + +fn assert_text_at(s: &Session, at: Vec2) { + assert!(s.running.is_none()); + let (dm, g) = last_dim(s); + assert!(dm.user_text_pos); + assert!(dm.text_mid.xy().dist(at) < 1e-9, "{:?}", dm.text_mid); + assert!(g.text_pos.dist(at) < 1e-9, "{:?}", g.text_pos); + let nearest = g.lines_of(LineRole::Dim).flatten().map(|p| p.dist(at)).fold(f64::INFINITY, f64::min); + assert!(nearest > 0.05, "the leader runs into the text: {nearest}"); +} + +#[test] +fn radial_text_goes_to_a_location_outside_the_circle() { + let mut s = Session::new(); + s.script("circle 50,50 25\n").unwrap(); + s.script("dimradius 75,50 100,80\n").unwrap(); + assert_text_at(&s, Vec2::new(100.0, 80.0)); + + s.script("circle 300,50 25\n").unwrap(); + s.script("dimdiameter 325,50 350,80\n").unwrap(); + assert_text_at(&s, Vec2::new(350.0, 80.0)); + + s.execute("arc", &json!({"center": [50, 200], "radius": 25, "start": 0, "end": 90})).unwrap(); + s.script("dimradius 75,200 100,230\n").unwrap(); + assert_text_at(&s, Vec2::new(100.0, 230.0)); + + // Radius 4 | 32 | 128, diameter 3 | 32 | 128, as AutoCAD writes them. + assert_eq!(dxf_flags(&s), vec![164, 163, 164]); +} diff --git a/crates/render/src/dim.rs b/crates/render/src/dim.rs index a57142bad..f38e7ea0d 100644 --- a/crates/render/src/dim.rs +++ b/crates/render/src/dim.rs @@ -777,7 +777,16 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font end(&mut g, blk1, start, -u); } } - g.dim(vec![tip, tpos]); + // Text the user placed: the leader stops short of it by the offset the default + // placement leaves between leader end and text. + let lead = if !d.user_text_pos { + tpos + } else if horizontal { + text_mid - Vec2::new(tw / 2.0 + gap, 0.0) * if u.x >= 0.0 { 1.0 } else { -1.0 } + } else { + text_mid - u * (tw / 2.0 + gap) + }; + g.dim(vec![tip, lead]); } end(&mut g, blk2, tip, u); if horizontal {