View-dependent display refinement: finer curved faces and edges as you zoom in (#1320) (#1425)

* View-dependent display refinement: bodies seen up close are drawn from finer display plans (#1320)

Each frame the viewport works out every shown body's screen-space error (the display
quality's chord tolerance in pixels where the body is nearest the eye) and picks a
refinement step past the chosen display quality, each halving the chord, with hysteresis.
Steps are planned off the UI thread and swapped in when ready, within a triangle budget
and a cache size. Faces (GPU plan or CPU mesh), edges, silhouettes and highlights of a
refined body all come from the refined plan, so a highlight stays on the drawn triangles.
Planned bodies are chunked by content on the GPU, so a refinement re-evaluates only its
own chunk. Display only: the reference mesh, the B-rep and exports never change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Display refinement: only collect finished plans while the view stands still, time the choice, keep the web planning one step a frame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the Display quality note as it was (the longer one pushed the Colour vision list off the Graphics page)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-15 11:03:53 -04:00
1 parent 8b97b9ed62
commit d43c414b7b
7 files changed
+805 -14

No files matched your search

+14 -1
View File
@@ -295,6 +295,12 @@ fn joined<'a>(blocks: impl Iterator<Item = &'a Option<Batch>>, own: Option<Batch
(!parts.is_empty()).then_some(Batch { parts, chunk })
}
/// Planned bodies end a GPU chunk one time in this many (by their plan's address), and a chunk
/// holds at most [`MAX_CHUNK_BODIES`]: a scene of a thousand bodies takes a few dozen dispatches
/// once, and a changed body re-evaluates a few dozen bodies, not all of them.
const CHUNK_SPLIT: u64 = 32;
const MAX_CHUNK_BODIES: usize = 128;
#[derive(Default)]
struct Batches {
/// The scene blocks on the GPU, by key.
@@ -381,18 +387,25 @@ impl Batches {
encoder: &mut wgpu::CommandEncoder,
bodies: &[PlannedBody],
) -> Vec<PlannedDraw> {
// Chunks end where a body's own plan says (or when full), so a body whose plan changes
// (an edit, or a refinement as the view zooms in, #1320) re-evaluates only its own
// chunk, not the whole scene.
let mut chunks: Vec<Vec<&PlannedBody>> = Vec::new();
let mut filled = 0usize;
let mut open = false;
for b in bodies {
let n = b.plan.vertex_count();
match chunks.last_mut() {
Some(c) if filled + n <= solidcraft_gpu::MAX_BATCH => c.push(b),
Some(c) if open && filled + n <= solidcraft_gpu::MAX_BATCH => c.push(b),
_ => {
chunks.push(vec![b]);
filled = 0;
}
}
filled += n;
let mut h = std::collections::hash_map::DefaultHasher::new();
std::hash::Hash::hash(&(Arc::as_ptr(&b.plan) as usize), &mut h);
open = !std::hash::Hasher::finish(&h).is_multiple_of(CHUNK_SPLIT) && chunks.last().is_some_and(|c| c.len() < MAX_CHUNK_BODIES);
}
let key = |c: &[&PlannedBody]| c.iter().map(|b| Arc::as_ptr(&b.plan) as usize).collect::<Vec<usize>>();
self.resident.retain(|k, _| chunks.iter().any(|c| key(c) == *k));