From d6a6b266759935e2aab9859e2121a5904710f4a5 Mon Sep 17 00:00:00 2001 From: kevin9327 Date: Mon, 14 Sep 2026 08:59:58 +0900 Subject: [PATCH] fix(step): export mesh vertices with their low residual at large coordinates Scene meshes store each position as an f32 half plus a low residual so geometry stays precise at survey coordinates, and the renderer adds the two back together. STEP export read only the f32 half. Near x = 500000 that half steps in 1/32 units, so a solid at 500000.123 was written as 500000.125, and the edge directions and planes were built from the same snapped points. Build each exported vertex from both halves in f64 and derive directions and the fallback face normal from those. Meshes without a residual export as before. Co-Authored-By: Claude Opus 5 --- src/io/step.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 7 deletions(-) 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));")); + } +}