diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 5e14e994a..455be1429 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -1274,11 +1274,13 @@ impl OpenCADStudio { match truck_tess::tessellate_solid(&solid, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, @@ -1352,11 +1354,13 @@ impl OpenCADStudio { match truck_tess::tessellate_shell(&shell, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, @@ -1437,11 +1441,13 @@ impl OpenCADStudio { match truck_tess::tessellate_solid(&solid, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, @@ -1454,11 +1460,13 @@ impl OpenCADStudio { match truck_tess::tessellate_shell(&shell, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, @@ -1486,11 +1494,13 @@ impl OpenCADStudio { match truck_tess::tessellate_solid(&solid, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, @@ -1503,11 +1513,13 @@ impl OpenCADStudio { match truck_tess::tessellate_shell(&shell, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, @@ -1593,11 +1605,13 @@ impl OpenCADStudio { match truck_tess::tessellate_shell(&shell, woff) { truck_tess::TruckTessResult::Mesh { verts, + verts_low, normals, indices, } => Some(crate::scene::model::mesh_model::MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, diff --git a/src/io/obj.rs b/src/io/obj.rs index 925c1a32f..5bd61b29e 100644 --- a/src/io/obj.rs +++ b/src/io/obj.rs @@ -103,6 +103,7 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option { Some(MeshModel { name: String::new(), verts, + verts_low: Vec::new(), normals: norms, indices, color, diff --git a/src/scene/convert/acis_to_truck.rs b/src/scene/convert/acis_to_truck.rs index 9c134d3bc..6246875ea 100644 --- a/src/scene/convert/acis_to_truck.rs +++ b/src/scene/convert/acis_to_truck.rs @@ -110,6 +110,7 @@ pub fn tessellate_sat_truck( let mut mesh = MeshModel { name: name.clone(), verts: Vec::new(), + verts_low: Vec::new(), normals: Vec::new(), indices: Vec::new(), color, diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 140578d58..142d3b3ca 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -125,6 +125,7 @@ fn tessellate_sat( Some(MeshModel { name, verts, + verts_low: Vec::new(), normals, indices, color, diff --git a/src/scene/convert/truck_tess.rs b/src/scene/convert/truck_tess.rs index 185f8f258..8a5268143 100644 --- a/src/scene/convert/truck_tess.rs +++ b/src/scene/convert/truck_tess.rs @@ -78,9 +78,11 @@ pub enum TruckTessResult { /// returned as parallel double-single (high, low) f32 buffers so the GPU /// keeps sub-unit precision at UTM-scale coordinates. Lines(Vec<[f32; 3]>, Vec<[f32; 3]>), - /// A triangle mesh (from Face, Shell, or Solid). + /// A triangle mesh (from Face, Shell, or Solid). `verts` is the high half + /// of the double-single position; `verts_low` the paired residual. Mesh { verts: Vec<[f32; 3]>, + verts_low: Vec<[f32; 3]>, normals: Vec<[f32; 3]>, indices: Vec, }, @@ -177,6 +179,7 @@ pub fn tessellate_shell(s: &Shell, offset: [f64; 3]) -> TruckTessResult { pub fn tessellate_shell(_s: &Shell, _offset: [f64; 3]) -> TruckTessResult { TruckTessResult::Mesh { verts: Vec::new(), + verts_low: Vec::new(), normals: Vec::new(), indices: Vec::new(), } @@ -197,6 +200,7 @@ pub fn tessellate_solid(s: &Solid, offset: [f64; 3]) -> TruckTessResult { pub fn tessellate_solid(_s: &Solid, _offset: [f64; 3]) -> TruckTessResult { TruckTessResult::Mesh { verts: Vec::new(), + verts_low: Vec::new(), normals: Vec::new(), indices: Vec::new(), } @@ -206,11 +210,14 @@ pub fn tessellate_solid(_s: &Solid, _offset: [f64; 3]) -> TruckTessResult { #[cfg(feature = "solid3d")] fn polygon_to_result(mesh: PolygonMesh, offset: [f64; 3]) -> TruckTessResult { - let verts: Vec<[f32; 3]> = mesh - .positions() - .iter() - .map(|p| to_local(p.x, p.y, p.z, offset)) - .collect(); + let positions = mesh.positions(); + let mut verts: Vec<[f32; 3]> = Vec::with_capacity(positions.len()); + let mut verts_low: Vec<[f32; 3]> = Vec::with_capacity(positions.len()); + for p in positions.iter() { + let hi = to_local(p.x, p.y, p.z, offset); + verts_low.push(to_local_low(p.x, p.y, p.z, offset, hi)); + verts.push(hi); + } // Per-vertex normals: if the mesh has normals, map each triangle vertex's // normal index back to the normals array. Fall back to empty if absent. @@ -262,6 +269,7 @@ fn polygon_to_result(mesh: PolygonMesh, offset: [f64; 3]) -> TruckTessResult { TruckTessResult::Mesh { verts, + verts_low, normals, indices, } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 30cebfae6..3d1375adb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -557,20 +557,31 @@ fn overlap_len(a: (f32, f32), b: (f32, f32)) -> f32 { /// uses the same space. fn offset_mesh_lod_set(mut set: MeshLodSet, world_offset: [f64; 3]) -> MeshLodSet { let [ox, oy, oz] = world_offset; - let (fx, fy, fz) = (ox as f32, oy as f32, oz as f32); let mut min_x = f32::INFINITY; let mut min_y = f32::INFINITY; let mut max_x = f32::NEG_INFINITY; let mut max_y = f32::NEG_INFINITY; for lod in &mut set.lods { - for v in &mut lod.verts { - v[0] -= fx; - v[1] -= fy; - v[2] -= fz; - if v[0] < min_x { min_x = v[0]; } - if v[1] < min_y { min_y = v[1]; } - if v[0] > max_x { max_x = v[0]; } - if v[1] > max_y { max_y = v[1]; } + // Reconstruct the f64 absolute position from the double-single pair, + // subtract world_offset in f64, then re-split into (high, low) so the + // relative-to-eye shader keeps sub-unit precision at UTM scale. + let has_low = lod.verts_low.len() == lod.verts.len(); + if !has_low { + lod.verts_low = vec![[0.0; 3]; lod.verts.len()]; + } + for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) { + let ax = v[0] as f64 + vl[0] as f64 - ox; + let ay = v[1] as f64 + vl[1] as f64 - oy; + let az = v[2] as f64 + vl[2] as f64 - oz; + let hx = ax as f32; + let hy = ay as f32; + let hz = az as f32; + *v = [hx, hy, hz]; + *vl = [(ax - hx as f64) as f32, (ay - hy as f64) as f32, (az - hz as f64) as f32]; + if hx < min_x { min_x = hx; } + if hy < min_y { min_y = hy; } + if hx > max_x { max_x = hx; } + if hy > max_y { max_y = hy; } } } if min_x.is_finite() { @@ -596,15 +607,30 @@ fn transform_block_mesh_lod_set( let mut max_x = f32::NEG_INFINITY; let mut max_y = f32::NEG_INFINITY; for lod in &mut out.lods { - for v in &mut lod.verts { - let w = xform.apply(Vector3::new(v[0] as f64, v[1] as f64, v[2] as f64)); - v[0] = (w.x - ox) as f32; - v[1] = (w.y - oy) as f32; - v[2] = (w.z - oz) as f32; - if v[0] < min_x { min_x = v[0]; } - if v[1] < min_y { min_y = v[1]; } - if v[0] > max_x { max_x = v[0]; } - if v[1] > max_y { max_y = v[1]; } + let has_low = lod.verts_low.len() == lod.verts.len(); + if !has_low { + lod.verts_low = vec![[0.0; 3]; lod.verts.len()]; + } + for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) { + // Reconstruct the block-local f64, apply the INSERT transform and + // subtract world_offset in f64, then re-split into (high, low). + let w = xform.apply(Vector3::new( + v[0] as f64 + vl[0] as f64, + v[1] as f64 + vl[1] as f64, + v[2] as f64 + vl[2] as f64, + )); + let ax = w.x - ox; + let ay = w.y - oy; + let az = w.z - oz; + let hx = ax as f32; + let hy = ay as f32; + let hz = az as f32; + *v = [hx, hy, hz]; + *vl = [(ax - hx as f64) as f32, (ay - hy as f64) as f32, (az - hz as f64) as f32]; + if hx < min_x { min_x = hx; } + if hy < min_y { min_y = hy; } + if hx > max_x { max_x = hx; } + if hy > max_y { max_y = hy; } } for n in &mut lod.normals { let d = xform.apply_rotation(Vector3::new(n[0] as f64, n[1] as f64, n[2] as f64)); diff --git a/src/scene/model/mesh_model.rs b/src/scene/model/mesh_model.rs index a626159f8..1ca21184e 100644 --- a/src/scene/model/mesh_model.rs +++ b/src/scene/model/mesh_model.rs @@ -9,8 +9,11 @@ pub struct MeshModel { /// Unique identifier (entity handle value as decimal string). pub name: String, - /// World-space vertex positions. + /// World-space vertex positions (high half of the double-single pair). pub verts: Vec<[f32; 3]>, + /// Low residual paired with `verts` so meshes stay precise at UTM scale. + /// Empty = all-zero (legacy / interactive meshes near the origin). + pub verts_low: Vec<[f32; 3]>, /// Per-vertex normals (may be empty if not available). pub normals: Vec<[f32; 3]>, /// Triangle indices into `verts` (every 3 values = one triangle). diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index 7c0d42ba9..cdfa91c52 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -172,12 +172,14 @@ pub fn mesh_from_solid(solid: &Solid, color: [f32; 4]) -> Option { match tessellate_solid(solid, [0.0; 3]) { TruckTessResult::Mesh { verts, + verts_low, normals, indices, } if !indices.is_empty() => { let mesh = MeshModel { name: String::new(), verts, + verts_low, normals, indices, color, diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index cc6b181f9..c2b1c4bf9 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -19,30 +19,37 @@ pub struct MeshVertex { pub position: [f32; 3], pub normal: [f32; 3], pub color: [f32; 4], + pub position_low: [f32; 3], } impl MeshVertex { pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + const ATTRS: &[wgpu::VertexAttribute] = &[ + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, position) as u64, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, normal) as u64, + shader_location: 1, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, color) as u64, + shader_location: 2, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, position_low) as u64, + shader_location: 3, + format: wgpu::VertexFormat::Float32x3, + }, + ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: 12, - shader_location: 1, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: 24, - shader_location: 2, - format: wgpu::VertexFormat::Float32x4, - }, - ], + attributes: ATTRS, } } } @@ -133,6 +140,7 @@ impl MeshGpu { position: pos, normal, color, + position_low: mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]), } }) .collect(); diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index b1eb5ba44..654b6fc86 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -30,9 +30,10 @@ struct Uniforms { var u: Uniforms; struct VertexIn { - @location(0) position: vec3, - @location(1) normal: vec3, - @location(2) color: vec4, + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) color: vec4, + @location(3) position_low: vec3, }; struct VertexOut { @@ -45,7 +46,8 @@ struct VertexOut { @vertex fn vs_main(v: VertexIn) -> VertexOut { var out: VertexOut; - out.clip_pos = u.view_rot * vec4((v.position - u.eye_high) - u.eye_low, 1.0); + let rel = (v.position - u.eye_high) + (v.position_low - u.eye_low); + out.clip_pos = u.view_rot * vec4(rel, 1.0); out.color = v.color; out.normal = v.normal; out.world_pos = v.position;