fix(solid3d): keep loaded-solid mesh verts f64 until the RTE split

Loaded 3DSOLID faces rendered on a ~0.06 m grid and crawled against their own
wireframe at UTM scale. Both ACIS tessellation paths cast each vertex f64→f32 in
the samplers, and only split the double-single low half (verts_low) when the
body carried a `transform` record. But many files — budweiser among them — store
solid geometry in absolute coordinates with no body transform, so verts_low
stayed empty: the GPU got position_low = 0 and reconstructed an already
f32-quantized absolute position, while the feature edges (split unconditionally
from f64) stayed exact.

Both paths now accumulate vertices in f64 and hand them to one finalize step:

  finalize_mesh(name, verts_f64, normals, indices, color, xform)

the only place a solid vertex becomes f32 — it computes the world coordinate in
f64 (body placement applied, or identity), then splits into (verts, verts_low)
unconditionally, exactly as attach_feature_edges already does for the wireframe.
push_quad / tess_plane_face / tess_cone_face / tess_sphere_face /
tess_torus_face / tess_spline_face and the truck append_group / bespoke_face /
append_spline_faces all carry f64; apply_body_transform is gone.

Verified on budweiser (all solids, body_transform = NONE): verts_low is now
populated with the captured residual, max |low| ≈ 0.0624 m. The Model-tab
(truck_tess::polygon_to_result) and edge paths were already correct and are
unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-18 12:30:00 +03:00
1 parent 608cf8c74b
commit 340a358c26
3 files changed
+115 -91

No files matched your search

+39 -41
View File
@@ -29,11 +29,11 @@ use acadrust::entities::acis::{
};
use crate::scene::convert::solid3d_tess::{
apply_body_transform, body_transform, collect_face_loops, cone_axis_span, tess_cone_face,
body_transform, collect_face_loops, cone_axis_span, finalize_mesh, tess_cone_face,
tess_plane_face, tess_sphere_face, tess_torus_face, LodConfig, BOUNDARY_CHORD_FRAC,
TRUCK_CHORD_FRAC,
};
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
use crate::scene::model::mesh_model::MeshLodSet;
/// Slightly over 2π so revolution builders close the loop.
const FULL: f64 = std::f64::consts::TAU + 0.2;
@@ -85,15 +85,12 @@ pub fn tessellate_sat_truck(
color: [f32; 4],
_facet_res: f64,
) -> Option<MeshLodSet> {
let mut mesh = MeshModel {
name: name.clone(),
verts: Vec::new(),
verts_low: Vec::new(),
normals: Vec::new(),
indices: Vec::new(),
color,
selected: false,
};
// Vertices accumulate in f64 world/local space; `finalize_mesh` splits them
// into the double-single (high, low) pair once, so a solid at UTM scale
// keeps full precision instead of quantizing to the f32 grid.
let mut verts: Vec<[f64; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
for face in sat.faces().into_iter() {
let Some(surf_rec) = sat.resolve(face.surface()) else {
@@ -105,7 +102,7 @@ pub fn tessellate_sat_truck(
let shell: Shell = faces.into();
let poly = shell.triangulation(tol).to_polygon();
if !poly.tri_faces().is_empty() {
append_group(&mut mesh, &poly, &outward);
append_group(&mut verts, &mut normals, &mut indices, &poly, &outward);
appended = true;
}
}
@@ -113,25 +110,22 @@ pub fn tessellate_sat_truck(
// Truck's rsweep/triangulation degenerates to zero triangles on some
// short-arc cones (a curved wall face whose profile passes through the
// local origin), leaving a see-through gap. Fall back to the bespoke
// parametric sampler for just that face — it writes body-local verts
// into the same buffers, so the shared body transform below still
// applies uniformly.
// parametric sampler for just that face — it writes into the same
// buffers, so the shared finalize below still applies uniformly.
if !appended {
bespoke_face(sat, &face, surf_rec, &mut mesh);
bespoke_face(sat, &face, surf_rec, &mut verts, &mut normals, &mut indices);
}
}
// Spline (NURBS) faces are meshed by direct grid sampling of the truck
// BSplineSurface — see spline_tess — and merged into the same buffers.
append_spline_faces(sat, &mut mesh);
append_spline_faces(sat, &mut verts, &mut normals, &mut indices);
if mesh.indices.is_empty() {
if indices.is_empty() {
return None;
}
if let Some((m, tr, scale)) = body_transform(sat) {
apply_body_transform(&mut mesh, &m, &tr, scale);
}
let mesh = finalize_mesh(name, verts, normals, indices, color, body_transform(sat));
Some(MeshLodSet::from_lods(vec![mesh]))
}
@@ -142,9 +136,10 @@ fn bespoke_face(
sat: &SatDocument,
face: &SatFace,
surf_rec: &acadrust::entities::acis::SatRecord,
mesh: &mut MeshModel,
v: &mut Vec<[f64; 3]>,
n: &mut Vec<[f32; 3]>,
i: &mut Vec<u32>,
) {
let (v, n, i) = (&mut mesh.verts, &mut mesh.normals, &mut mesh.indices);
match surf_rec.entity_type.as_str() {
"plane-surface" => {
if let Some(p) = SatPlaneSurface::from_record(surf_rec) {
@@ -413,8 +408,12 @@ fn cone_boundary_arc(
/// Append meshes for every `spline-surface` face, reusing the truck
/// BSplineSurface grid sampler in `spline_tess`.
fn append_spline_faces(sat: &SatDocument, mesh: &mut MeshModel) {
use crate::scene::convert::solid3d_tess::LodConfig;
fn append_spline_faces(
sat: &SatDocument,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
for face in sat.faces() {
let Some(surf_rec) = sat.resolve(face.surface()) else {
continue;
@@ -422,21 +421,14 @@ fn append_spline_faces(sat: &SatDocument, mesh: &mut MeshModel) {
if surf_rec.entity_type != "spline-surface" {
continue;
}
let mut verts: Vec<[f32; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
crate::scene::convert::spline_tess::tess_spline_face(
sat,
&face,
LodConfig::HIGH,
&mut verts,
&mut normals,
&mut indices,
verts,
normals,
indices,
);
let base = mesh.verts.len() as u32;
mesh.verts.extend_from_slice(&verts);
mesh.normals.extend_from_slice(&normals);
mesh.indices.extend(indices.iter().map(|i| i + base));
}
}
@@ -445,14 +437,20 @@ fn append_spline_faces(sat: &SatDocument, mesh: &mut MeshModel) {
/// Append one face's triangulation to `mesh`, computing smooth per-vertex
/// normals from the outward rule and flipping any triangle whose winding
/// disagrees with that outward direction.
fn append_group(mesh: &mut MeshModel, poly: &PolygonMesh, outward: &Outward) {
fn append_group(
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
out_indices: &mut Vec<u32>,
poly: &PolygonMesh,
outward: &Outward,
) {
let positions = poly.positions();
let base = mesh.verts.len() as u32;
let base = verts.len() as u32;
for p in positions {
let pos = [p.x, p.y, p.z];
mesh.verts.push([p.x as f32, p.y as f32, p.z as f32]);
verts.push(pos);
let n = outward.at(pos);
mesh.normals.push([n[0] as f32, n[1] as f32, n[2] as f32]);
normals.push([n[0] as f32, n[1] as f32, n[2] as f32]);
}
for tri in poly.tri_faces() {
let (i0, i1, i2) = (tri[0].pos, tri[1].pos, tri[2].pos);
@@ -468,9 +466,9 @@ fn append_group(mesh: &mut MeshModel, poly: &PolygonMesh, outward: &Outward) {
let out = outward.at(cen);
let (j0, j1, j2) = (base + i0 as u32, base + i1 as u32, base + i2 as u32);
if vdot(gn, out) < 0.0 {
mesh.indices.extend_from_slice(&[j0, j2, j1]);
out_indices.extend_from_slice(&[j0, j2, j1]);
} else {
mesh.indices.extend_from_slice(&[j0, j1, j2]);
out_indices.extend_from_slice(&[j0, j1, j2]);
}
}
}
+74 -48
View File
@@ -113,13 +113,16 @@ pub(crate) fn nominal_segs(frac: f64) -> usize {
// ── Public entry point ────────────────────────────────────────────────────────
/// Tessellate a SAT document into mesh buffers — shared by all ACIS entities.
/// Vertices accumulate in f64 and `finalize_mesh` splits them into the
/// double-single pair with the body placement (`xform`) applied.
fn tessellate_sat(
sat: &SatDocument,
name: String,
color: [f32; 4],
lod: LodConfig,
xform: Option<([f64; 9], [f64; 3], f64)>,
) -> Option<MeshModel> {
let mut verts: Vec<[f32; 3]> = Vec::new();
let mut verts: Vec<[f64; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
@@ -197,15 +200,7 @@ fn tessellate_sat(
if indices.is_empty() {
return None;
}
Some(MeshModel {
name,
verts,
verts_low: Vec::new(),
normals,
indices,
color,
selected: false,
})
Some(finalize_mesh(name, verts, normals, indices, color, xform))
}
/// Tessellate an ACIS document, preferring the truck B-rep kernel and falling
@@ -879,10 +874,7 @@ fn tessellate_sat_lods(
let mut lods: Vec<MeshModel> = Vec::with_capacity(3);
for lod in configs {
let scaled = scale_lod(lod, facet_res);
if let Some(mut m) = tessellate_sat(sat, name.clone(), color, scaled) {
if let Some((mat, tr, scale)) = xform {
apply_body_transform(&mut m, &mat, &tr, scale);
}
if let Some(m) = tessellate_sat(sat, name.clone(), color, scaled, xform) {
lods.push(m);
}
}
@@ -921,38 +913,69 @@ pub(crate) fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f
/// Apply a body placement transform to a mesh. ACIS treats points as row
/// 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) {
// 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];
/// Build a `MeshModel` from f64 accumulation buffers. This is the ONLY place a
/// solid mesh vertex becomes f32: the world coordinate is computed in f64 (the
/// body placement applied, or identity when the solid stores absolute geometry)
/// and split into the double-single (high, low) pair the mesh shader
/// reconstructs relative to the eye — exactly the treatment the feature edges
/// get in `attach_feature_edges`. Casting to f32 any earlier quantizes a solid
/// placed at UTM scale to a ~0.06 m grid, so its shaded faces crawl against
/// their own (double-single) wireframe as the camera moves. The split runs
/// unconditionally: many solids store their geometry in absolute coordinates
/// with no body transform, and those need it just as much as placed ones.
pub(crate) fn finalize_mesh(
name: String,
verts: Vec<[f64; 3]>,
normals: Vec<[f32; 3]>,
indices: Vec<u32>,
color: [f32; 4],
xform: Option<([f64; 9], [f64; 3], f64)>,
) -> MeshModel {
let mut hi: Vec<[f32; 3]> = Vec::with_capacity(verts.len());
let mut lo: Vec<[f32; 3]> = Vec::with_capacity(verts.len());
for [x, y, z] in verts {
let (wx, wy, wz) = match &xform {
Some((m, tr, scale)) => (
scale * (x * m[0] + y * m[3] + z * m[6]) + tr[0],
scale * (x * m[1] + y * m[4] + z * m[7]) + tr[1],
scale * (x * m[2] + y * m[5] + z * m[8]) + tr[2],
),
None => (x, y, z),
};
let (hx, hy, hz) = (wx as f32, wy as f32, wz as f32);
mesh.verts[i] = [hx, hy, hz];
mesh.verts_low.push([
hi.push([hx, hy, hz]);
lo.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);
let nx = x * m[0] + y * m[3] + z * m[6];
let ny = x * m[1] + y * m[4] + z * m[7];
let nz = x * m[2] + y * m[5] + z * m[8];
let len = (nx * nx + ny * ny + nz * nz).sqrt();
if len > 1e-9 {
*n = [(nx / len) as f32, (ny / len) as f32, (nz / len) as f32];
}
let normals = match &xform {
Some((m, _, _)) => normals
.iter()
.map(|n| {
let (x, y, z) = (n[0] as f64, n[1] as f64, n[2] as f64);
let nx = x * m[0] + y * m[3] + z * m[6];
let ny = x * m[1] + y * m[4] + z * m[7];
let nz = x * m[2] + y * m[5] + z * m[8];
let len = (nx * nx + ny * ny + nz * nz).sqrt();
if len > 1e-9 {
[(nx / len) as f32, (ny / len) as f32, (nz / len) as f32]
} else {
*n
}
})
.collect(),
None => normals,
};
MeshModel {
name,
verts: hi,
verts_low: lo,
normals,
indices,
color,
selected: false,
}
}
@@ -1314,10 +1337,13 @@ pub(crate) fn resolve_point(sat: &SatDocument, v_ptr: SatPointer) -> Option<[f64
// ── Mesh builder helpers ──────────────────────────────────────────────────────
/// Append one quad (two triangles) to the mesh buffers.
/// Append one quad (two triangles) to the mesh buffers. Vertices stay in f64
/// world/local space until `finalize_mesh` splits them into the double-single
/// (high, low) pair — casting here would quantize a solid placed at UTM scale to
/// the ~0.06 m f32 grid.
#[inline]
fn push_quad(
verts: &mut Vec<[f32; 3]>,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
p: [[f64; 3]; 4],
@@ -1326,7 +1352,7 @@ fn push_quad(
let base = verts.len() as u32;
let nf = [n[0] as f32, n[1] as f32, n[2] as f32];
for &pt in &p {
verts.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
verts.push(pt);
normals.push(nf);
}
// Two CCW triangles: (0,1,2) and (0,2,3)
@@ -1340,7 +1366,7 @@ pub(crate) fn tess_plane_face(
face: &SatFace,
plane: &SatPlaneSurface,
chord_frac: f64,
verts: &mut Vec<[f32; 3]>,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
@@ -1364,7 +1390,7 @@ pub(crate) fn tess_plane_face(
let base = verts.len() as u32;
for &pt in &poly {
verts.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
verts.push(pt);
normals.push(nf);
}
@@ -1383,7 +1409,7 @@ pub(crate) fn tess_cone_face(
face: &SatFace,
cone: &SatConeSurface,
lod: LodConfig,
verts: &mut Vec<[f32; 3]>,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
@@ -1658,7 +1684,7 @@ pub(crate) fn tess_sphere_face(
face: &SatFace,
sphere: &SatSphereSurface,
lod: LodConfig,
verts: &mut Vec<[f32; 3]>,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
@@ -1746,7 +1772,7 @@ pub(crate) fn tess_torus_face(
face: &SatFace,
torus: &SatTorusSurface,
lod: LodConfig,
verts: &mut Vec<[f32; 3]>,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
+2 -2
View File
@@ -56,7 +56,7 @@ pub fn tess_spline_face(
sat: &SatDocument,
face: &SatFace,
lod: LodConfig,
verts: &mut Vec<[f32; 3]>,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
@@ -90,7 +90,7 @@ pub fn tess_spline_face(
let u = u0 + (u1 - u0) * (i as f64 / su as f64);
let p = surface.subs(u, v);
let n = surface.normal(u, v);
verts.push([p.x as f32, p.y as f32, p.z as f32]);
verts.push([p.x, p.y, p.z]);
normals.push([n.x as f32, n.y as f32, n.z as f32]);
}
}