fix(leader): suppress oversized arrowheads, keep them precise at UTM

Three defects made a leader's arrowhead render as one stray line far
longer than the leader itself, at UTM coordinates:

- The arrowhead was drawn even when it is larger than the first leader
  segment it sits on. A DIMASZ sized for a bigger plot (here 6 units on a
  ~1-4 unit pointer) painted a stroke longer than the whole leader.
  AutoCAD omits the arrowhead when it does not fit the first segment, so
  gate the arrow on `arrow_size <= first_segment_len`.

- Leader geometry was built as absolute f32. At UTM coordinates (Y ~ 4.5M,
  f32 ulp ~ 0.5) the oblique arrow tick's small cross-slope quantised
  away, collapsing the 45-degree tick into a flat horizontal line. Build
  the path and arrow in absolute f64 — the arrow around a local origin so
  append_arrow's f32 math stays precise — then RTE-split into the double
  single (points_low / fill_tris_low) at the end.

- The `_Oblique` / `_Archtick` arrowhead block mapped to ArrowKind::Tick
  with the full DIMASZ, but the tick emitter draws `size` to either side
  (half-length semantics, matching DIMTSZ). Pass half so an oblique
  arrowhead sizes to DIMASZ like every other arrowhead, not double.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-23 16:46:55 +03:00
1 parent ec92a0cdc6
commit 2d3f33de10
2 files changed
+48 -26

No files matched your search

+42 -25
View File
@@ -614,7 +614,6 @@ impl LeaderTess for Leader {
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
[(v.x) as f32, (v.y) as f32, (v.z) as f32]
};
let nan = [f32::NAN; 3];
let verts = &self.vertices;
@@ -648,10 +647,14 @@ impl LeaderTess for Leader {
};
}
let mut points: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
// Build all geometry in absolute f64 and RTE-split at the end. At UTM
// coordinates a raw `v.x as f32` quantises to ~0.5 units, which flattens
// the oblique arrow tick into a horizontal line and jitters the path.
let origin = [verts[0].x, verts[0].y, verts[0].z];
let mut points: Vec<[f64; 3]> = verts.iter().map(|v| [v.x, v.y, v.z]).collect();
let mut tangents: Vec<TangentGeom> = Vec::new();
let key_vertices: Vec<[f64; 3]> = verts.iter().map(|v| [v.x, v.y, v.z]).collect();
let mut fill_tris: Vec<[f32; 3]> = Vec::new();
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
for i in 0..verts.len().saturating_sub(1) {
tangents.push(TangentGeom::Line {
@@ -698,19 +701,32 @@ impl LeaderTess for Leader {
let dy = (next.y - tip.y) as f32;
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
let dir = Vec3::new(dx / len, dy / len, 0.0);
let tip_f = p3(tip);
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
// Reuse the dim arrow emitter so the leader shape matches the
// DIMSTYLE in use (Closed Filled by default, Dot, Tick, …).
let mut arrow_pts: Vec<[f32; 3]> = Vec::new();
let mut arrow_geom = DimGeom::new();
append_arrow(&mut arrow_geom, tip_v, dir, &arrow);
if !arrow_geom.dim_lines.is_empty() {
arrow_pts.push(nan);
arrow_pts.extend(arrow_geom.dim_lines);
// Omit the arrowhead when it is larger than the first leader segment
// it would sit on: an oversized arrow on a short pointer is
// suppressed, not drawn overshooting the segment. Without this an
// annotation-scaled arrow (DIMASZ sized for a bigger plot) paints a
// stray stroke longer than the leader itself.
if arrow_size <= len {
// Emit the arrow around a local origin (the tip) so
// `append_arrow`'s f32 math stays precise, then lift each point
// back to absolute f64. Reuse the dim arrow emitter so the
// leader shape matches the DIMSTYLE in use (Closed Filled by
// default, Dot, Tick, …).
let mut arrow_geom = DimGeom::new();
append_arrow(&mut arrow_geom, Vec3::ZERO, dir, &arrow);
let lift = |p: &[f32; 3]| -> [f64; 3] {
[
p[0] as f64 + origin[0],
p[1] as f64 + origin[1],
p[2] as f64 + origin[2],
]
};
if !arrow_geom.dim_lines.is_empty() {
points.push([f64::NAN; 3]);
points.extend(arrow_geom.dim_lines.iter().map(&lift));
}
fill_tris.extend(arrow_geom.arrow_fill.iter().map(&lift));
}
points.extend(arrow_pts);
fill_tris.extend(arrow_geom.arrow_fill);
}
if draws_hookline(self) {
@@ -755,16 +771,17 @@ impl LeaderTess for Leader {
}
}
}
let last_f = p3(last);
points.push(nan);
points.push(last_f);
points.push([
last_f[0] + (sign * land_len) as f32,
last_f[1],
last_f[2],
]);
points.push([f64::NAN; 3]);
points.push([last.x, last.y, last.z]);
points.push([last.x + sign * land_len, last.y, last.z]);
}
// Split absolute f64 into the render/eye double-single so the GPU keeps
// full precision at UTM coordinates (empty low bufs = absolute f32).
let (points, points_low) = crate::scene::convert::tessellate::points_to_ds(points);
let (fill_tris, fill_tris_low) =
crate::scene::convert::tessellate::points_to_ds(fill_tris);
WireModel {
taper_widths: Vec::new(),
world_width: 0.0,
@@ -777,7 +794,7 @@ impl LeaderTess for Leader {
text_verts: Vec::new(),
name,
points,
points_low: Vec::new(),
points_low,
color,
selected,
aci: 0,
@@ -790,7 +807,7 @@ impl LeaderTess for Leader {
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris,
fill_tris_low: Vec::new(),
fill_tris_low,
}
}
}
+6 -1
View File
@@ -1551,7 +1551,12 @@ fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind {
"ORIGIN" | "ORIGIN2" | "ORIGININDICATOR" | "ORIGININDICATOR2" => {
ArrowKind::Origin { size: dimasz }
}
"OBLIQUE" | "ARCHTICK" => ArrowKind::Tick { size: dimasz },
// `ArrowKind::Tick` draws the stroke `size` to either side of the tip
// (total 2·size — its `size` is a half-length, matching DIMTSZ). As an
// arrowhead block, though, DIMASZ is the stroke's *full* length like
// every other arrowhead here, so pass half of it — otherwise the
// oblique tick renders twice the intended DIMASZ.
"OBLIQUE" | "ARCHTICK" => ArrowKind::Tick { size: dimasz * 0.5 },
"BOXFILLED" => ArrowKind::Box_ {
size: dimasz,
filled: true,