forked from 1b-agent/opencadstudio-upstream
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:
1 parent
608cf8c74b
commit
340a358c26
3 files changed
+115
-91
No files matched your search
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
) {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user