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 <[email protected]>
This commit is contained in:
kevin9327andClaude Opus 5 committed 2026-09-14 08:59:58 +09:00
1 parent 7b4cef41f7
commit d6a6b26675
1 file changed
+75 -7
+75 -7
View File
@@ -21,7 +21,7 @@ use std::fmt::Write as FmtWrite;
pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
// 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<String> {
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<String> {
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<String> {
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<String> {
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));"));
}
}