diff --git a/crates/engine/src/cmd/annotate.rs b/crates/engine/src/cmd/annotate.rs index 9b235d4..f69ed7f 100644 --- a/crates/engine/src/cmd/annotate.rs +++ b/crates/engine/src/cmd/annotate.rs @@ -232,13 +232,14 @@ fn circle_of(s: &Session, h: Handle) -> Option<(Vec2, f64)> { fn radial(s: &mut Session, p: &Value, diameter: bool) -> Result { let id = if diameter { "dimdiameter" } else { "dimradius" }; - let (c, pt) = if let (Some(c), Some(pt)) = (point_param(p, "center"), point_param(p, "point")) { - (c, pt) + let (c, pt, at) = if let (Some(c), Some(pt)) = (point_param(p, "center"), point_param(p, "point")) { + (c, pt, None) } else { let h = targets(s, p)?.first().copied().ok_or_else(|| bad(id, "`handle` of a circle/arc, or {center, point}"))?; let (c, r) = circle_of(s, h).ok_or_else(|| bad(id, "object is not a circle or arc"))?; - let at = point_param(p, "at").unwrap_or(c + Vec2::from_angle(std::f64::consts::FRAC_PI_4)); - (c, c + (at - c).normalized() * r) + let at = point_param(p, "at"); + let dir = at.unwrap_or(c + Vec2::from_angle(std::f64::consts::FRAC_PI_4)); + (c, c + (dir - c).normalized() * r, at) }; if c.near(pt, 1e-12) { return Err(bad(id, "`point` must differ from `center`")); @@ -248,9 +249,25 @@ fn radial(s: &mut Session, p: &Value, diameter: bool) -> Result { } else { 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), + 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 + { + d.text_mid = v3(at); + d.user_text_pos = true; + } + k +} + fn run_radius(s: &mut Session, p: &Value) -> Result { radial(s, p, false) } @@ -1018,11 +1035,12 @@ impl RadialM { } fn kind(&self, s: &Session, c: Vec2, r: f64, at: Vec2) -> EntityKind { let pt = c + (at - c).normalized() * r; - self.text.apply(if self.diameter { + let k = self.text.apply(if self.diameter { dim(s, DimKind::Diameter, c + (c - pt), Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "") } else { dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "") - }) + }); + text_inside(k, c, r, at) } } diff --git a/crates/engine/tests/dim_radial_inside.rs b/crates/engine/tests/dim_radial_inside.rs new file mode 100644 index 0000000..d16dac0 --- /dev/null +++ b/crates/engine/tests/dim_radial_inside.rs @@ -0,0 +1,52 @@ +//! DIMRADIUS / DIMDIAMETER: a dimension line location picked inside the circle puts the text +//! there, on the dimension line; outside, the text stays beyond the arrowhead. + +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:?}"), + } +} + +/// Whether a dimension line passes over `p` (on the x axis). +fn covers(g: &DimGeometry, x: f64) -> bool { + g.lines_of(LineRole::Dim) + .any(|l| l.windows(2).any(|w| w[0].y.abs() < 1e-9 && w[1].y.abs() < 1e-9 && w[0].x.min(w[1].x) < x && x < w[0].x.max(w[1].x))) +} + +#[test] +fn radial_text_goes_inside_when_the_location_is_inside() { + let mut s = Session::new(); + s.execute("circle", &json!({"center": [0, 0], "radius": 10})).unwrap(); + + // Typed: pick the circle, then a location inside. + s.script("dimradius 10,0 4,0\n").unwrap(); + assert!(s.running.is_none()); + let (dm, g) = last_dim(&s); + assert!(dm.user_text_pos); + assert!(g.text_pos.dist(Vec2::new(4.0, 0.0)) < 1e-9, "{:?}", g.text_pos); + // The dimension line runs from beyond the text to the arrowhead and stays inside the circle. + 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. + 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); + assert!(g.text_pos.y > 10.0, "{:?}", g.text_pos); + + // JSON form, diameter: the line spans the circle, broken around the text. + let c = s.doc().unwrap().model.iter().next().unwrap().handle.hex(); + s.execute("dimdiameter", &json!({"handle": c, "at": [-3, 0]})).unwrap(); + let (_, g) = last_dim(&s); + assert!(g.text_pos.dist(Vec2::new(-3.0, 0.0)) < 1e-9, "{:?}", g.text_pos); + assert!(covers(&g, -9.5) && covers(&g, 9.5) && !covers(&g, -3.0)); +} diff --git a/crates/render/src/dim.rs b/crates/render/src/dim.rs index de7d1b9..4d02c3b 100644 --- a/crates/render/src/dim.rs +++ b/crates/render/src/dim.rs @@ -544,19 +544,40 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font let (center, r) = if radius { (a, a.dist(b)) } else { (a.mid(b), a.dist(b) / 2.0) }; let meas = if radius { r } else { 2.0 * r }; g.mtext = linear_text(meas, d, st, if radius { "R" } else { "%%c" }); - let (tw, _) = measure(&g.mtext); + let (tw, tht) = measure(&g.mtext); let u = (b - center).normalized(); let u = if u == Vec2::ZERO || !u.is_finite() { Vec2::X } else { u }; let tip = center + u * r; let tpos = if d.user_text_pos { text_mid } else { tip + u * (asz.max(tsz) * 3.0) }; - if !radius { - let start = center - u * r; - g.dim(vec![start, tip]); - end(&mut g, blk1, start, -u); + // Text placed inside the circle (a dimension line location picked inside): the text + // sits on the dimension line, which runs from the text out to the arrowhead (for a + // diameter it spans the circle, broken around the text). DIMTIH turns it horizontal. + let inside = d.user_text_pos && text_mid.dist(center) < r; + let horizontal = if inside { st.text_inside_horizontal } else { st.text_outside_horizontal }; + if inside { + let tr = Vec2::from_angle(if horizontal { 0.0 } else { readable(u.angle()) }); + let half = if tw > 0.0 { (tw * tr.dot(u).abs() + tht * tr.perp().dot(u).abs()) / 2.0 + gap } else { 0.0 }; + let start = if radius { center } else { center - u * r }; + let (t0, t1) = ((text_mid - start).dot(u) - half, (text_mid - start).dot(u) + half); + if !radius { + if t0 > 0.0 { + g.dim(vec![start, start + u * t0]); + } + end(&mut g, blk1, start, -u); + } + if t1 < tip.dist(start) { + g.dim(vec![start + u * t1.max(0.0), tip]); + } + } else { + if !radius { + let start = center - u * r; + g.dim(vec![start, tip]); + end(&mut g, blk1, start, -u); + } + g.dim(vec![tip, tpos]); } - g.dim(vec![tip, tpos]); end(&mut g, blk2, tip, u); - if st.text_outside_horizontal { + if horizontal { text_angle = 0.0; let sx = if u.x >= 0.0 { 1.0 } else { -1.0 }; if !d.user_text_pos {