forked from 1b-agent/opencadstudio-upstream
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:
1 parent
7b4cef41f7
commit
d6a6b26675
1 file changed
+75
-7
+75
-7
@@ -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));"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user