DIMRADIUS/DIMDIAMETER: a location inside the circle puts the text inside (fixes #385) (#389)

Only the direction of the picked dimension line location was used, so
the text always went outside beyond the arrowhead. A location inside
the circle (typed, picked or JSON `at`) now places the text there: the
dimension stores it as its text position, and the dimension line runs
from the text out to the arrowhead; a diameter's spans the circle,
broken around the text. DIMTIH (not DIMTOH) decides whether inside
text is horizontal. Outside locations keep the default placement.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein 2026-10-11 00:09:05 +02:00 committed by GitHub
parent 2f4ceea7a9
commit 1418130d45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 104 additions and 13 deletions

View File

@ -232,13 +232,14 @@ fn circle_of(s: &Session, h: Handle) -> Option<(Vec2, f64)> {
fn radial(s: &mut Session, p: &Value, diameter: bool) -> Result<Value> {
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<Value> {
} 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<Value> {
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)
}
}

View File

@ -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));
}

View File

@ -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 {