From 26e83a90bfce027ec395938fdb6bcaa3f8a385d3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 17 Jul 2026 19:15:02 +0300 Subject: [PATCH] fix(solid3d): double-single split for shaded ACIS faces at UTM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 3D solid's shaded faces used absolute f32 world coordinates while its feature edges carried the double-single (high, low) pair the mesh shader reconstructs relative to the eye. At UTM scale the faces snapped to a ~0.06 m grid, so the shaded surface crawled against its own wireframe as the camera moved. `apply_body_transform` is where a solid gets its world placement — the large body translation is exactly where the coordinate stops fitting an f32. It now computes each world vertex in f64 and splits it into the pair, filling `verts_low` the same way the feature edges already are. Both tessellation paths (truck B-rep and the bespoke sampler) route through it, and the downstream re-split points (top-level offset, per-INSTANCE block transform) already consume the pair, so they inherit the corrected residual. Picking, which reads `verts_low` too, sharpens at UTM as well. Co-Authored-By: Claude Opus 4.8 --- src/scene/convert/solid3d_tess.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 1beb0b681..c5b33aa7f 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -297,12 +297,27 @@ pub(crate) fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f /// vectors (`p' = scale·(p·M) + T`), so the 3×3 is indexed transposed relative /// to a column-vector multiply. Normals get the rotation only, renormalized. pub(crate) fn apply_body_transform(mesh: &mut MeshModel, m: &[f64; 9], tr: &[f64; 3], scale: f64) { - for v in &mut mesh.verts { - let (x, y, z) = (v[0] as f64, v[1] as f64, v[2] as f64); + // The body translation `tr` is where a solid gets its world placement, so at + // UTM scale this is exactly where the coordinate stops fitting in an f32. + // Compute each world vertex in f64 and split it into the double-single + // (high, low) pair the mesh shader reconstructs relative to the eye — the + // same treatment the feature edges already get. Without the low half the + // shaded faces sit on a ~0.06 m grid while their own edges stay exact, so the + // surface visibly crawls against its wireframe as the camera moves. + mesh.verts_low = Vec::with_capacity(mesh.verts.len()); + for i in 0..mesh.verts.len() { + let [vx, vy, vz] = mesh.verts[i]; + let (x, y, z) = (vx as f64, vy as f64, vz as f64); let wx = scale * (x * m[0] + y * m[3] + z * m[6]) + tr[0]; let wy = scale * (x * m[1] + y * m[4] + z * m[7]) + tr[1]; let wz = scale * (x * m[2] + y * m[5] + z * m[8]) + tr[2]; - *v = [wx as f32, wy as f32, wz as f32]; + let (hx, hy, hz) = (wx as f32, wy as f32, wz as f32); + mesh.verts[i] = [hx, hy, hz]; + mesh.verts_low.push([ + (wx - hx as f64) as f32, + (wy - hy as f64) as f32, + (wz - hz as f64) as f32, + ]); } for n in &mut mesh.normals { let (x, y, z) = (n[0] as f64, n[1] as f64, n[2] as f64); @@ -1142,5 +1157,3 @@ fn norm3(v: [f64; 3]) -> [f64; 3] { [v[0] / len, v[1] / len, v[2] / len] } } - -