forked from 1b-agent/opencadstudio-upstream
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:
1 parent
a2fb334966
commit
e191315713
4 files changed
+115
-43
No files matched your search
+21
-9
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user