diff --git a/src/entities/ray.rs b/src/entities/ray.rs index a1f6277dc..77960aae6 100644 --- a/src/entities/ray.rs +++ b/src/entities/ray.rs @@ -16,10 +16,17 @@ impl TruckConvertible for Ray { fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { 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 { 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]), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 8cc60b1c4..a210b6d6d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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); } diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 80de1ae8c..7801d2564 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -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; } diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index 6c76e139f..1dedb5d4f 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -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 {