perf(render): batch solid meshes, fix per-pick freezes on heavy 3D models

Opening / navigating a heavy 3D DWG (e.g. a BIM model with ~10k ACIS solids)
was effectively frozen. Several independent costs, all addressed here:

- Face3D edge/fill buffers were re-uploaded on every camera move (keyed on
  camera_generation). They are world-space and selection-independent, so gate
  them on (geometry_epoch, fill_mode) instead — a pan no longer re-walks every
  wire.
- Solid meshes were drawn one buffer + one draw call per solid (~10k draws/
  frame), which strangled the GPU front end. Concatenate every solid's LOD0
  into a few large buffers (split only to stay under the 256 MB buffer cap) and
  draw the whole set in a handful of calls. Built once per geometry epoch, so
  selection / hover never re-pack it.
- Selection / hover highlight is now a small tinted overlay of just the picked
  solids, drawn last with an Always-depth pipeline so it shows on top even when
  the solid is occluded — instead of re-tinting and re-uploading all meshes.
- Hover / pick hit-testing scanned every triangle of every solid each move
  (seconds on a heavy model). Add a 3D-AABB screen-rect broad phase
  (MeshLodSet now carries z_aabb) and reuse the renderer's cached expanded mesh
  set, so picking is O(solids) cheap projections plus the few solids actually
  under the cursor.
- Block-internal hatch hit-testing exploded every INSERT on every hover.
  Cache the result per geometry epoch and skip blocks that contain no hatch.
- Curved-surface (cone/sphere/torus) tessellation tolerance is now radius-
  relative (CURVE_REL_TOL) so a cylinder's facet count is size-independent and
  matches the circle/arc wire tessellation, instead of exploding on large radii.

Also adds docs/tessellation.md mapping every EntityType to its tessellation
path (truck B-rep vs truck curve topology vs direct).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-30 22:46:29 +03:00
1 parent 27896e3401
commit a994a148fe
10 files changed
+572 -146

No files matched your search

+45
View File
@@ -138,6 +138,51 @@ pub fn click_hits_all<'a>(
/// mesh triangle to screen space and test whether the cursor lands inside it.
/// Returns the front-most hit (smallest projected depth). Lets meshed solids
/// (whose only wire geometry is thin edges) be selected on their faces.
/// Conservative broad-phase: does the solid's 3D AABB project to a screen rect
/// that the cursor falls within (± `CLICK_THRESHOLD_PX`)? Lets the caller skip
/// ray-testing the triangles of solids whose footprint isn't under the cursor —
/// O(solids) cheap projections instead of O(triangles) per hover. Returns
/// `true` (don't cull) whenever any corner is at/behind the camera, where the
/// projected rect can't be trusted.
///
/// `world_aabb` is `[min_x, min_y, max_x, max_y]`, `z_aabb` is `[min_z, max_z]`,
/// and `view_proj` is the same matrix `mesh_click_hit` projects vertices with.
pub fn aabb_under_cursor(
world_aabb: [f32; 4],
z_aabb: [f32; 2],
cursor: Point,
view_proj: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
) -> bool {
let [min_x, min_y, max_x, max_y] = world_aabb;
let [min_z, max_z] = z_aabb;
if !min_x.is_finite() || !min_z.is_finite() {
return true; // degenerate bound — don't cull
}
let (mut sx0, mut sy0, mut sx1, mut sy1) =
(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
for &z in &[min_z, max_z] {
for &(x, y) in &[(min_x, min_y), (max_x, min_y), (min_x, max_y), (max_x, max_y)] {
let rel = glam::DVec3::new(x as f64 - eye.x, y as f64 - eye.y, z as f64 - eye.z)
.as_vec3();
let clip = view_proj * rel.extend(1.0);
if clip.w <= 1e-6 {
return true; // corner at/behind the camera — can't bound; keep it
}
let ndc = clip.truncate() / clip.w;
let px = (ndc.x + 1.0) * 0.5 * bounds.width;
let py = (1.0 - ndc.y) * 0.5 * bounds.height;
sx0 = sx0.min(px);
sy0 = sy0.min(py);
sx1 = sx1.max(px);
sy1 = sy1.max(py);
}
}
let m = CLICK_THRESHOLD_PX;
cursor.x >= sx0 - m && cursor.x <= sx1 + m && cursor.y >= sy0 - m && cursor.y <= sy1 + m
}
pub fn mesh_click_hit<'a>(
cursor: Point,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,