feat(ucs): orient text-bearing annotations to the active UCS

New annotations now place their text and reading geometry along the
active UCS X axis (identity = world), so they read square to the user's
coordinate system instead of world-horizontal:

- TEXT / MTEXT: rotation defaults to the UCS X angle
  (DocumentTab::ucs_rotation_angle, model space only).
- Linear dimensions: measure along the UCS axis the span is nearest, and
  bake the UCS X angle as the text rotation so the value reads square
  even under a style that forces horizontal text. DIMCONTINUE / DIMBASELINE
  derive their axis from the base dim's rotation (any angle), not a
  world H/V test.
- LEADER: the MText annotation rotates to the UCS, and the hookline runs
  along the leader's horizontal_direction (set to UCS X), used by the
  renderer instead of world horizontal.
- MLEADER: context.text_direction = UCS X drives both the text rotation
  and, in the renderer, the text side and dogleg/landing direction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-22 18:18:35 +03:00
commit 33314a5889
10 changed files with 160 additions and 73 deletions

View file

@ -65,7 +65,19 @@ fn to_truck(leader: &Leader) -> TruckEntity {
if leader.hookline_enabled && verts.len() >= 2 {
let last = verts.last().unwrap();
let prev = &verts[verts.len() - 2];
let sign = if (last.x - prev.x) >= 0.0 {
// Landing runs along the leader's horizontal direction (UCS X for
// UCS-placed leaders, world X otherwise), on the side the leader
// approaches from.
let (hx, hy) = {
let h = leader.horizontal_direction;
let l = (h.x * h.x + h.y * h.y).sqrt();
if l > 1e-9 {
(h.x / l, h.y / l)
} else {
(1.0, 0.0)
}
};
let sign = if (last.x - prev.x) * hx + (last.y - prev.y) * hy >= 0.0 {
1.0_f64
} else {
-1.0_f64
@ -74,7 +86,11 @@ fn to_truck(leader: &Leader) -> TruckEntity {
let last_f = p3(last);
points.push(nan);
points.push(last_f);
points.push([last_f[0] + sign * len, last_f[1], last_f[2]]);
points.push([
last_f[0] + sign * len * hx,
last_f[1] + sign * len * hy,
last_f[2],
]);
}
TruckEntity {

View file

@ -102,11 +102,22 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
.copied()
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
.unwrap_or(text_loc);
let text_sign: f64 = if text_loc.x >= leader_ref.x {
1.0
} else {
-1.0
// Reading direction of the text / landing — the stored text_direction
// (UCS X for UCS-placed leaders, world X otherwise), so the whole text-side
// layout follows the UCS instead of world horizontal.
let (tdx, tdy) = {
let td = ml.context.text_direction;
let l = (td.x * td.x + td.y * td.y).sqrt();
if l > 1e-9 {
(td.x / l, td.y / l)
} else {
let a = ml.context.text_rotation;
(a.cos(), a.sin())
}
};
// Which side of the leader the text sits on, measured along that direction.
let rel_dot = (text_loc.x - leader_ref.x) * tdx + (text_loc.y - leader_ref.y) * tdy;
let text_sign: f64 = if rel_dot >= 0.0 { 1.0 } else { -1.0 };
// Lay the text out once, up front: its width fixes where the landing meets
// the text. The same layout is reused below to emit the glyph strokes.
@ -164,7 +175,11 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
};
// text_location is the leader-facing (near) edge; the landing runs one
// dogleg from there back toward the leader.
let landing_pt = [text_loc.x - text_sign * dogleg, text_loc.y, text_loc.z];
let landing_pt = [
text_loc.x - text_sign * dogleg * tdx,
text_loc.y - text_sign * dogleg * tdy,
text_loc.z,
];
for root in &ml.context.leader_roots {
let cp = &root.connection_point;