fix: restore visibility for large-coordinate DXF files after world_offset regression

Three root causes caused all geometry and UCS arrows to disappear:

1. TruckObject::Lines and TruckObject::Text variants returned world-space
   f32 coordinates without applying world_offset, so fit_all() computed
   bounds spanning from local space to UTM world space (~4 M m), placing
   the camera impossibly far from all visible geometry.

2. Ray/XLine entities with unnormalized direction vectors (common in some
   DXF exporters) produced astronomically large far-point coordinates
   (up to 1e28) that overwhelmed the camera fit even after the offset fix.
   Fixed by normalizing the direction vector before multiplying by
   DISPLAY_EXTENT.

3. Origin-stuck corrupted entities (coordinates ≈ 0 in world space become
   ≈ -world_offset in local space) pulled the fit_all() bounds far from
   the actual drawing. Fixed by computing local_extent_max from EXTMIN/EXTMAX
   (10× safety margin) and skipping points outside that range in fit_all().

Additional hardening: wire_gpu.rs now skips ±inf segments (not just NaN),
and snap_pts/key_vertices in all TruckObject variants are offset-corrected.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-27 18:04:35 +03:00
1 parent a2fb334966
commit e191315713
4 files changed
+115 -43

No files matched your search

+21 -9
View File
@@ -16,10 +16,17 @@ impl TruckConvertible for Ray {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
let bp = self.base_point;
let dir = self.direction;
// Normalize direction to avoid f32 overflow when DXF stores
// unnormalized direction vectors (garbage data in some exporters).
let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt();
if len < 1e-10 {
return None;
}
let (nx, ny, nz) = (dir.x / len, dir.y / len, dir.z / len);
let far = [
(bp.x + dir.x * DISPLAY_EXTENT) as f32,
(bp.y + dir.y * DISPLAY_EXTENT) as f32,
(bp.z + dir.z * DISPLAY_EXTENT) as f32,
(bp.x + nx * DISPLAY_EXTENT) as f32,
(bp.y + ny * DISPLAY_EXTENT) as f32,
(bp.z + nz * DISPLAY_EXTENT) as f32,
];
let start = [bp.x as f32, bp.y as f32, bp.z as f32];
Some(TruckEntity {
@@ -138,15 +145,20 @@ impl TruckConvertible for XLine {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
let bp = self.base_point;
let dir = self.direction;
let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt();
if len < 1e-10 {
return None;
}
let (nx, ny, nz) = (dir.x / len, dir.y / len, dir.z / len);
let far_pos = [
(bp.x + dir.x * DISPLAY_EXTENT) as f32,
(bp.y + dir.y * DISPLAY_EXTENT) as f32,
(bp.z + dir.z * DISPLAY_EXTENT) as f32,
(bp.x + nx * DISPLAY_EXTENT) as f32,
(bp.y + ny * DISPLAY_EXTENT) as f32,
(bp.z + nz * DISPLAY_EXTENT) as f32,
];
let far_neg = [
(bp.x - dir.x * DISPLAY_EXTENT) as f32,
(bp.y - dir.y * DISPLAY_EXTENT) as f32,
(bp.z - dir.z * DISPLAY_EXTENT) as f32,
(bp.x - nx * DISPLAY_EXTENT) as f32,
(bp.y - ny * DISPLAY_EXTENT) as f32,
(bp.z - nz * DISPLAY_EXTENT) as f32,
];
Some(TruckEntity {
object: TruckObject::Lines(vec![far_neg, far_pos]),
+31 -5
View File
@@ -126,6 +126,10 @@ pub struct Scene {
/// Scene centroid subtracted from all coordinates before f32 conversion.
/// Eliminates f32 precision loss at large world coordinates (e.g. UTM 4,000,000 m).
pub world_offset: [f64; 3],
/// Largest local-space coordinate expected from real geometry, derived from
/// EXTMIN/EXTMAX (10× safety margin). Used by fit_all() to ignore garbage
/// entity coordinates (origin-stuck entities, bad Ray/XLine direction vectors).
pub local_extent_max: f32,
}
impl Scene {
@@ -157,6 +161,7 @@ impl Scene {
bg_color: [0.11, 0.11, 0.11, 1.0],
paper_bg_color: [1.0, 1.0, 1.0, 1.0],
world_offset: [0.0; 3],
local_extent_max: 1e9,
}
}
@@ -173,15 +178,22 @@ impl Scene {
// Sentinel: DXF files with no geometry have EXTMIN = 1e20, EXTMAX = -1e20.
// Fall back to no offset so the scene stays at the origin.
let valid = min.x < max.x && min.y < max.y;
self.world_offset = if valid {
[
if valid {
self.world_offset = [
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
(min.z + max.z) * 0.5,
]
];
// 10× the EXTMIN→EXTMAX half-diagonal gives fit_all() a threshold to
// reject origin-stuck/corrupted entities far outside the drawing area.
let hw = ((max.x - min.x) * 0.5) as f32;
let hh = ((max.y - min.y) * 0.5) as f32;
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
self.local_extent_max = hw.max(hh).max(hz) * 10.0;
} else {
[0.0; 3]
};
self.world_offset = [0.0; 3];
self.local_extent_max = 1e9;
}
}
/// Public accessor for the block-record handle of the current layout.
@@ -2316,12 +2328,26 @@ impl Scene {
let mut min = glam::Vec3::splat(f32::MAX);
let mut max = glam::Vec3::splat(f32::MIN);
// Reject points beyond 10× the drawing's EXTMIN→EXTMAX half-size.
// These come from origin-stuck entities (y ≈ -world_offset.y after subtraction),
// Ray/XLine far-points with bad direction vectors, or corrupt coordinates.
let lim = self.local_extent_max;
for wire in &wires {
for &[x, y, z] in &wire.points {
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
continue;
}
if x.abs() > lim || y.abs() > lim || z.abs() > lim {
continue;
}
min = min.min(glam::Vec3::new(x, y, z));
max = max.max(glam::Vec3::new(x, y, z));
}
}
// If no usable points found, leave the camera unchanged.
if min.x > max.x {
return;
}
if min == max {
max += glam::Vec3::splat(1.0);
}
+12 -12
View File
@@ -150,9 +150,9 @@ impl WireGpu {
for i in 1..n {
let p = wire.points[i - 1];
let q = wire.points[i];
// If either point is a NaN sentinel, keep the same distance
// (the segment will be skipped anyway).
if p[0].is_nan() || q[0].is_nan() {
// If either point is non-finite (NaN sentinel or ±inf from overflow),
// keep the same distance — the segment will be skipped anyway.
if !p[0].is_finite() || !q[0].is_finite() {
dists[i] = dists[i - 1];
} else {
let dx = q[0] - p[0];
@@ -166,15 +166,15 @@ impl WireGpu {
let a = wire.points[i];
let b = wire.points[i + 1];
// Skip any segment that involves a NaN sentinel point.
// NaN sentinels are inserted between disconnected glyph strokes
// by tessellate.rs so that multiple strokes can share one WireModel.
if a[0].is_nan()
|| a[1].is_nan()
|| a[2].is_nan()
|| b[0].is_nan()
|| b[1].is_nan()
|| b[2].is_nan()
// Skip segments where either endpoint is non-finite (NaN sentinels
// from disconnected glyph strokes, or ±inf from Ray/XLine far-point
// overflow when the direction vector is very large).
if !a[0].is_finite()
|| !a[1].is_finite()
|| !a[2].is_finite()
|| !b[0].is_finite()
|| !b[1].is_finite()
|| !b[2].is_finite()
{
continue;
}
+51 -17
View File
@@ -65,8 +65,11 @@ pub fn tessellate(
match te.object {
// ── Text / MText: pre-tessellated glyph strokes ───────────────
TruckObject::Text(strokes_2d) => {
// Elevation comes from the entity's Z coordinate.
let elev = entity_z(entity);
// Glyph strokes come from acad_to_truck in world-space f32.
// Subtract world_offset (f32 subtraction — acceptable precision
// for text rendering; the anchor was already cast to f32 there).
let [ox, oy, oz] = world_offset;
let elev = entity_z(entity) - oz as f32;
// Pack all strokes into one flat point list, separated by
// NaN sentinels so wire_gpu.rs skips disconnected segments.
@@ -81,10 +84,11 @@ pub fn tessellate(
points.push([f32::NAN, f32::NAN, f32::NAN]);
}
for &[x, y] in stroke {
points.push([x, y, elev]);
points.push([x - ox as f32, y - oy as f32, elev]);
}
}
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
return WireModel {
name,
points,
@@ -93,7 +97,7 @@ pub fn tessellate(
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px,
snap_pts: te.snap_pts,
snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices,
@@ -107,6 +111,7 @@ pub fn tessellate(
match result {
TruckTessResult::Point([x, y, z]) => {
let s = 0.1_f32;
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
return WireModel {
name,
points: vec![
@@ -120,11 +125,11 @@ pub fn tessellate(
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: te.snap_pts,
snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
};
}
_ => {}
@@ -133,6 +138,11 @@ pub fn tessellate(
TruckObject::Curve(e) => {
if let TruckTessResult::Lines(points) = tessellate_edge(&e, world_offset) {
let [ox, oy, oz] = world_offset;
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te.key_vertices.into_iter()
.map(|[x, y, z]| [x - ox as f32, y - oy as f32, z - oz as f32])
.collect();
return WireModel {
name,
points,
@@ -141,17 +151,22 @@ pub fn tessellate(
pattern_length,
pattern,
line_weight_px,
snap_pts: te.snap_pts,
snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
};
}
}
TruckObject::Contour(w) => {
if let TruckTessResult::Lines(points) = tessellate_wire(&w, world_offset) {
let [ox, oy, oz] = world_offset;
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te.key_vertices.into_iter()
.map(|[x, y, z]| [x - ox as f32, y - oy as f32, z - oz as f32])
.collect();
return WireModel {
name,
points,
@@ -160,29 +175,41 @@ pub fn tessellate(
pattern_length,
pattern,
line_weight_px,
snap_pts: te.snap_pts,
snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
};
}
}
TruckObject::Lines(points) => {
// Points are world-space f32 from entity converters (polyline,
// leader, mesh, solid2d, etc.). Subtract world_offset so the
// geometry lands in local space alongside Line/Arc/Circle.
let [ox, oy, oz] = world_offset;
let local_pts: Vec<[f32; 3]> = points.into_iter().map(|[x, y, z]| {
if x.is_nan() { [x, y, z] }
else { [x - ox as f32, y - oy as f32, z - oz as f32] }
}).collect();
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te.key_vertices.into_iter()
.map(|[x, y, z]| [x - ox as f32, y - oy as f32, z - oz as f32])
.collect();
return WireModel {
name,
points,
points: local_pts,
color,
selected,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px,
snap_pts: te.snap_pts,
snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
};
}
@@ -956,6 +983,13 @@ fn vec3_local(v: Vector3, off: [f64; 3]) -> Vec3 {
Vec3::new((v.x - off[0]) as f32, (v.y - off[1]) as f32, (v.z - off[2]) as f32)
}
fn offset_snap_pts(pts: Vec<(Vec3, SnapHint)>, off: [f64; 3]) -> Vec<(Vec3, SnapHint)> {
let [ox, oy, oz] = off;
pts.into_iter()
.map(|(p, h)| (Vec3::new(p.x - ox as f32, p.y - oy as f32, p.z - oz as f32), h))
.collect()
}
/// Returns the text position of a dimension in DXF world-space (f64, no offset applied).
/// Used when building a synthetic Text entity so tessellate() can apply world_offset itself.
fn dimension_text_pos_f64(dim: &Dimension) -> Vector3 {