diff --git a/src/io/step.rs b/src/io/step.rs index 8846d2c95..e5648c68c 100644 --- a/src/io/step.rs +++ b/src/io/step.rs @@ -21,7 +21,7 @@ use std::fmt::Write as FmtWrite; pub fn build_step(meshes: &[&MeshModel]) -> Option { // Collect all triangles as (v0, v1, v2, normal). struct Tri { - v: [[f32; 3]; 3], + v: [[f64; 3]; 3], n: [f32; 3], } @@ -30,6 +30,13 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { let verts = &mesh.verts; let normals = &mesh.normals; let idx = &mesh.indices; + // Positions are stored as an f32 half plus a low residual so they stay + // precise at survey coordinates; write the sum, as the renderer uses. + let point = |i: usize| { + let high = verts[i]; + let low = mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]); + [0, 1, 2].map(|k| high[k] as f64 + low[k] as f64) + }; let n_tri = idx.len() / 3; for t in 0..n_tri { let i0 = idx[t * 3] as usize; @@ -38,9 +45,9 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { if i0 >= verts.len() || i1 >= verts.len() || i2 >= verts.len() { continue; } - let a = verts[i0]; - let b = verts[i1]; - let c = verts[i2]; + let a = point(i0); + let b = point(i1); + let c = point(i2); let n = if !normals.is_empty() && i0 < normals.len() { normals[i0] } else { @@ -49,8 +56,8 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { let nx = ab[1] * ac[2] - ab[2] * ac[1]; let ny = ab[2] * ac[0] - ab[0] * ac[2]; let nz = ab[0] * ac[1] - ab[1] * ac[0]; - let len = (nx * nx + ny * ny + nz * nz).sqrt().max(f32::EPSILON); - [nx / len, ny / len, nz / len] + let len = (nx * nx + ny * ny + nz * nz).sqrt().max(f64::EPSILON); + [(nx / len) as f32, (ny / len) as f32, (nz / len) as f32] }; tris.push(Tri { v: [a, b, c], n }); } @@ -128,7 +135,7 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { tri.v[k1][1] - tri.v[k][1], tri.v[k1][2] - tri.v[k][2], ]; - let len = (dx * dx + dy * dy + dz * dz).sqrt().max(f32::EPSILON); + let len = (dx * dx + dy * dy + dz * dz).sqrt().max(f64::EPSILON); writeln!( data, "#{} = DIRECTION('',({:.6},{:.6},{:.6}));", @@ -263,3 +270,64 @@ fn chrono_timestamp() -> String { let day = doy % 30 + 1; format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}") } + +#[cfg(test)] +mod tests { + use super::build_step; + use crate::scene::model::mesh_model::MeshModel; + + fn triangle(verts: Vec<[f32; 3]>, verts_low: Vec<[f32; 3]>) -> MeshModel { + MeshModel { + name: String::new(), + verts, + verts_low, + normals: Vec::new(), + indices: vec![0, 1, 2], + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), + color: [1.0; 4], + selected: false, + } + } + + /// Split an absolute position into the f32 pair the tessellator stores. + fn split(point: [f64; 3]) -> ([f32; 3], [f32; 3]) { + let high = point.map(|v| v as f32); + let low = [0, 1, 2].map(|i| (point[i] - high[i] as f64) as f32); + (high, low) + } + + /// At survey coordinates the f32 half alone is 6 cm coarse: 500000.123 + /// is stored as 500000.125 plus a -0.002 residual. The export has to add + /// the residual back, as the renderer does. + #[test] + fn large_coordinates_keep_their_low_residual() { + let (verts, verts_low): (Vec<_>, Vec<_>) = [ + [500000.123, 500010.456, 0.0], + [500001.123, 500010.456, 0.0], + [500000.123, 500011.456, 0.0], + ] + .into_iter() + .map(split) + .unzip(); + let step = build_step(&[&triangle(verts, verts_low)]).expect("step"); + assert!( + step.contains("CARTESIAN_POINT('',(500000.123000,500010.456000,0.000000));"), + "first vertex lost its residual:\n{}", + step.lines() + .find(|line| line.contains("CARTESIAN_POINT")) + .unwrap_or("") + ); + } + + /// Meshes without residuals (imported OBJ, legacy meshes) export as before. + #[test] + fn a_mesh_without_low_residuals_exports_its_positions() { + let mesh = triangle( + vec![[1.5, 2.0, 0.0], [2.5, 2.0, 0.0], [1.5, 3.0, 0.0]], + Vec::new(), + ); + let step = build_step(&[&mesh]).expect("step"); + assert!(step.contains("CARTESIAN_POINT('',(1.500000,2.000000,0.000000));")); + } +}