mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 19:15:57 +00:00
Section cap: only closed bodies take part in the crossing count
An open body (a surface body, a mesh that does not close) has no inside: its faces counted with the rest took the cap from the closed body it lay in or behind. The body's closedness, which the kernel knows (`ModelBody::is_closed`: no shell left open, a mesh body every side of which two triangles share), goes with it to the renderer (`SceneMesh`, `PlannedBody`, `InstancedMesh`, a block's closed runs, the loose open triangles), and the count draws closed bodies only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
+17
-4
@@ -73,8 +73,9 @@ pub struct ModelBody {
|
||||
/// [`kernel::measure`] of `body` (with the body it was measured on), worked out once and
|
||||
/// shared by clones: inspecting a design again measures only the bodies that changed.
|
||||
measured: Arc<OnceLock<(Body, kernel::Result<kernel::BodyMeasure>)>>,
|
||||
/// [`Body::is_surface`] of `body` (with the body it was measured on), worked out once and
|
||||
/// shared by clones.
|
||||
/// Whether `body` is open (with the body it was measured on): a B-rep one a surface body
|
||||
/// ([`Body::is_surface`]), a mesh one with a side only one triangle has
|
||||
/// ([`Body::is_closed_mesh`]); worked out once and shared by clones.
|
||||
surface: Arc<OnceLock<(Body, bool)>>,
|
||||
/// [`ModelBody::iso_lines`] worked out so far, shared by clones.
|
||||
iso: Arc<std::sync::Mutex<IsoLines>>,
|
||||
@@ -110,10 +111,22 @@ impl ModelBody {
|
||||
|
||||
/// A surface body: some shell does not close ([`Body::is_surface`]), worked out once.
|
||||
pub fn is_surface(&self) -> bool {
|
||||
match self.surface.get_or_init(|| (self.body.clone(), self.body.is_surface())) {
|
||||
!self.body.is_mesh() && self.is_open()
|
||||
}
|
||||
|
||||
/// The body encloses a volume: a solid whose shells all close ([`ModelBody::is_surface`]),
|
||||
/// or a mesh body every side of which two triangles share ([`Body::is_closed_mesh`]).
|
||||
/// Worked out once.
|
||||
pub fn is_closed(&self) -> bool {
|
||||
!self.is_open()
|
||||
}
|
||||
|
||||
fn is_open(&self) -> bool {
|
||||
let open = |b: &Body| if b.is_mesh() { !b.is_closed_mesh() } else { b.is_surface() };
|
||||
match self.surface.get_or_init(|| (self.body.clone(), open(&self.body))) {
|
||||
(measured, s) if measured.same(&self.body) => *s,
|
||||
// The body was replaced in this cell's holder since: look again.
|
||||
_ => self.body.is_surface(),
|
||||
_ => open(&self.body),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ fn scene(s: &Session) -> Option<(Scene, Camera)> {
|
||||
let face_colors =
|
||||
s.doc.face_colors(body).into_iter().map(|(face, look)| (face as u32, Rgb(look.color[0], look.color[1], look.color[2]))).collect();
|
||||
let opacity = s.doc.model_body_opacity(body) as f32;
|
||||
scene.meshes.push(SceneMesh { mesh, color, face_colors, opacity });
|
||||
scene.meshes.push(SceneMesh { mesh, color, face_colors, opacity, closed: body.is_closed() });
|
||||
}
|
||||
if bounds.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -676,7 +676,7 @@ fn build_scene(
|
||||
// A see-through appearance is drawn see-through, as the viewport draws it; a selected
|
||||
// body is drawn solid, so the selection colour reads.
|
||||
let opacity = if selected { 1.0 } else { s.doc.model_body_opacity(body) as f32 };
|
||||
sc.meshes.push(SceneMesh { mesh: Arc::clone(&mesh), color, face_colors, opacity });
|
||||
sc.meshes.push(SceneMesh { mesh: Arc::clone(&mesh), color, face_colors, opacity, closed: body.is_closed() });
|
||||
if cosmetic_threads
|
||||
&& thread_budget.available()
|
||||
&& s.doc.model_body_opacity(body) >= 0.999
|
||||
|
||||
@@ -424,8 +424,10 @@ pub fn scene_geometry(scene: &Scene) -> GpuScene {
|
||||
let n = mesh.normals.get(*k as usize).copied().filter(|n| n.is_finite() && n.len() > 0.5).unwrap_or(flat);
|
||||
if m.glass() {
|
||||
sc.glass_tri(p.to_f32(), n.to_f32(), col);
|
||||
} else {
|
||||
} else if m.closed {
|
||||
sc.tri(p.to_f32(), n.to_f32(), col);
|
||||
} else {
|
||||
sc.open_tri(p.to_f32(), n.to_f32(), col);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,7 +582,7 @@ mod tests {
|
||||
/// headless picture of a translucent target shows the tool inside it.
|
||||
#[test]
|
||||
fn a_see_through_body_is_glass_at_its_own_alpha_and_a_solid_one_is_not() {
|
||||
let mesh = |opacity: f32| SceneMesh { mesh: sheet(), color: Rgb(200, 30, 30), face_colors: Vec::new(), opacity };
|
||||
let mesh = |opacity: f32| SceneMesh { mesh: sheet(), color: Rgb(200, 30, 30), face_colors: Vec::new(), opacity, closed: true };
|
||||
let geometry = |opacity: f32| scene_geometry(&Scene { meshes: vec![mesh(opacity)], ..Default::default() });
|
||||
|
||||
let solid = geometry(1.0);
|
||||
@@ -604,7 +606,7 @@ mod tests {
|
||||
/// painting one face does not make the body solid again.
|
||||
#[test]
|
||||
fn a_painted_face_keeps_its_bodys_opacity() {
|
||||
let mesh = SceneMesh { mesh: sheet(), color: Rgb(200, 30, 30), face_colors: vec![(0, Rgb(0, 255, 0))], opacity: 0.5 };
|
||||
let mesh = SceneMesh { mesh: sheet(), color: Rgb(200, 30, 30), face_colors: vec![(0, Rgb(0, 255, 0))], opacity: 0.5, closed: true };
|
||||
let sc = scene_geometry(&Scene { meshes: vec![mesh], ..Default::default() });
|
||||
assert_eq!(sc.tris.len(), 0);
|
||||
assert_eq!(color(&sc, &sc.glass), Some([0, 255, 0, 128]), "the face's own colour at the body's alpha");
|
||||
|
||||
+97
-21
@@ -163,6 +163,10 @@ pub struct PlannedBody {
|
||||
/// How the whole body is drawn: opaque with the model, or faded or see-through blended
|
||||
/// order-independently (only on a renderer that does that: [`GpuTarget::oit`]).
|
||||
pub class: TriClass,
|
||||
/// The body encloses a volume (a solid, a closed mesh). Only closed bodies take part in a
|
||||
/// section's crossing count ([`Cut`]): an open one has no inside, so it neither gets a cap
|
||||
/// nor takes one from the body it lies in.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// What the viewport's last frame drew, counted as it was drawn (the performance overlay, #2548).
|
||||
@@ -403,6 +407,8 @@ pub struct InstancedMesh {
|
||||
/// in the part's frame): a placement small on screen is drawn from the coarsest whose error
|
||||
/// stays under [`LOD_PX`] there ([`Batches::cull`]).
|
||||
pub levels: Vec<(Arc<SharedTris>, f32)>,
|
||||
/// The triangles enclose a volume (see [`PlannedBody::closed`]).
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl InstancedMesh {
|
||||
@@ -447,6 +453,9 @@ pub struct GpuScene {
|
||||
pub palette: Arc<Palette>,
|
||||
/// Opaque triangles. A zero normal draws the colour flat (unlit).
|
||||
pub tris: Vec<u8>,
|
||||
/// Opaque triangles of open bodies, drawn as `tris` but left out of a section's crossing
|
||||
/// count (see [`PlannedBody::closed`]).
|
||||
pub open_tris: Vec<u8>,
|
||||
/// Opaque product PBR vertices; linear base color and explicit optical parameters.
|
||||
pub pbr: Vec<u8>,
|
||||
/// Translucent triangles (alpha from the colour), drawn after the opaque ones without
|
||||
@@ -538,6 +547,7 @@ impl GpuScene {
|
||||
}
|
||||
}
|
||||
out.tris.append(&mut self.tris);
|
||||
out.tris.append(&mut self.open_tris);
|
||||
out.trans.append(&mut self.trans);
|
||||
out.lines.append(&mut self.lines);
|
||||
out.hidden.append(&mut self.hidden);
|
||||
@@ -603,6 +613,9 @@ pub enum TriClass {
|
||||
pub struct BlockBytes {
|
||||
pub vertices: Arc<Vec<u8>>,
|
||||
pub opaque: Arc<Vec<std::ops::Range<u32>>>,
|
||||
/// The opaque runs of closed bodies: what a section's crossing count draws (see
|
||||
/// [`PlannedBody::closed`]).
|
||||
pub closed: Arc<Vec<std::ops::Range<u32>>>,
|
||||
pub glass: Arc<Vec<std::ops::Range<u32>>>,
|
||||
pub faded: Arc<Vec<std::ops::Range<u32>>>,
|
||||
pub lines: Arc<Vec<u8>>,
|
||||
@@ -634,6 +647,9 @@ pub fn extend_runs(runs: &mut Vec<std::ops::Range<u32>>, at: u32) {
|
||||
pub struct BlockBuilder {
|
||||
vertices: Vec<u8>,
|
||||
opaque: Vec<std::ops::Range<u32>>,
|
||||
closed: Vec<std::ops::Range<u32>>,
|
||||
/// The triangles being added belong to an open body ([`BlockBuilder::open_body`]).
|
||||
open: bool,
|
||||
glass: Vec<std::ops::Range<u32>>,
|
||||
faded: Vec<std::ops::Range<u32>>,
|
||||
lines: Vec<u8>,
|
||||
@@ -664,6 +680,16 @@ impl BlockBuilder {
|
||||
TriClass::Faded => &mut self.faded,
|
||||
};
|
||||
extend_runs(runs, at);
|
||||
if class == TriClass::Opaque && !self.open {
|
||||
extend_runs(&mut self.closed, at);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the triangles added from now on belong to an open body (a surface body, a mesh
|
||||
/// that does not close): those are left out of a section's crossing count. Closed until
|
||||
/// said otherwise.
|
||||
pub fn open_body(&mut self, open: bool) {
|
||||
self.open = open;
|
||||
}
|
||||
|
||||
/// The runs of `class` that fall within `vertices` (a body's share of the block).
|
||||
@@ -705,6 +731,7 @@ impl BlockBuilder {
|
||||
Arc::new(BlockBytes {
|
||||
vertices: Arc::new(self.vertices),
|
||||
opaque: Arc::new(self.opaque),
|
||||
closed: Arc::new(self.closed),
|
||||
glass: Arc::new(self.glass),
|
||||
faded: Arc::new(self.faded),
|
||||
lines: Arc::new(self.lines),
|
||||
@@ -727,6 +754,13 @@ impl GpuScene {
|
||||
}
|
||||
self.tris.extend_from_slice(&c.0.to_le_bytes());
|
||||
}
|
||||
/// A vertex of an open body's opaque triangle (see [`GpuScene::open_tris`]).
|
||||
pub fn open_tri(&mut self, p: [f32; 3], n: [f32; 3], c: PaletteIndex) {
|
||||
for v in p.iter().chain(&n) {
|
||||
self.open_tris.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
self.open_tris.extend_from_slice(&c.0.to_le_bytes());
|
||||
}
|
||||
/// A vertex of the ground-plane shadow (see [`GpuScene::shadow`]).
|
||||
pub fn shadow_tri(&mut self, p: [f32; 3], c: PaletteIndex) {
|
||||
for v in p.iter().chain(&[0.0; 3]) {
|
||||
@@ -1003,8 +1037,10 @@ struct Batch {
|
||||
struct PlannedDraw {
|
||||
mesh: Arc<crate::GpuMesh>,
|
||||
colors: wgpu::Buffer,
|
||||
/// Its bodies' class: a chunk holds bodies of one class.
|
||||
/// Its bodies' class and whether they are closed: a chunk holds bodies of one class, all
|
||||
/// closed or all open.
|
||||
class: TriClass,
|
||||
closed: bool,
|
||||
/// The address of each plan in the chunk, in `mesh.parts` order: a whole-body highlight
|
||||
/// finds the evaluated triangles of the body it lights.
|
||||
plans: Vec<usize>,
|
||||
@@ -1141,6 +1177,8 @@ struct LevelDraw {
|
||||
/// while they come back), and the placements in view, each drawn from its level.
|
||||
struct InstancedDraw {
|
||||
look: InstanceLook,
|
||||
/// Its triangles enclose a volume ([`InstancedMesh::closed`]).
|
||||
closed: bool,
|
||||
/// The full triangles first, then coarser ones.
|
||||
levels: Vec<LevelDraw>,
|
||||
bounds: [[f32; 3]; 2],
|
||||
@@ -1206,6 +1244,9 @@ struct BlockGpu {
|
||||
bytes: Arc<BlockBytes>,
|
||||
vertices: Option<wgpu::Buffer>,
|
||||
opaque: Option<ClassGpu>,
|
||||
/// The opaque runs of closed bodies ([`BlockBytes::closed`]): `opaque`'s own when they are
|
||||
/// all of them.
|
||||
closed: Option<ClassGpu>,
|
||||
glass: Option<ClassGpu>,
|
||||
faded: Option<ClassGpu>,
|
||||
lines: Option<Batch>,
|
||||
@@ -1272,6 +1313,12 @@ impl BlockGpu {
|
||||
class_gpu(device, fits, &bytes.vertices, bytes.runs(class), old_pair, same_vertices, uploaded)
|
||||
};
|
||||
let (opaque, glass, faded) = (class(TriClass::Opaque), class(TriClass::Glass), class(TriClass::Faded));
|
||||
let closed = if bytes.closed == bytes.opaque {
|
||||
opaque.clone()
|
||||
} else {
|
||||
let old_pair = old.as_ref().and_then(|o| o.closed.as_ref().map(|c| (c, &o.bytes.closed)));
|
||||
class_gpu(device, fits, &bytes.vertices, &bytes.closed, old_pair, same_vertices, uploaded)
|
||||
};
|
||||
let keep = |old: Option<&BlockGpu>, pick: fn(&BlockBytes) -> &Arc<Vec<u8>>, old_lines: fn(&BlockGpu) -> Option<&Batch>| {
|
||||
old.filter(|o| Arc::ptr_eq(pick(&o.bytes), pick(&bytes))).and_then(old_lines).cloned()
|
||||
};
|
||||
@@ -1281,7 +1328,7 @@ impl BlockGpu {
|
||||
};
|
||||
let lines = keep(old.as_ref(), |b| &b.lines, |b| b.lines.as_ref()).or_else(|| fresh(&bytes.lines, "sc_block_lines"));
|
||||
let hidden = keep(old.as_ref(), |b| &b.hidden, |b| b.hidden.as_ref()).or_else(|| fresh(&bytes.hidden, "sc_block_hidden"));
|
||||
BlockGpu { bytes, vertices, opaque, glass, faded, lines, hidden }
|
||||
BlockGpu { bytes, vertices, opaque, closed, glass, faded, lines, hidden }
|
||||
}
|
||||
|
||||
fn class(&self, class: TriClass) -> Option<&ClassGpu> {
|
||||
@@ -1294,7 +1341,16 @@ impl BlockGpu {
|
||||
|
||||
/// Draw `class`'s triangles with the pipeline already set (`tally`: what is counted).
|
||||
fn draw(&self, pass: &mut wgpu::RenderPass<'_>, class: TriClass, tally: &Tally) {
|
||||
match self.class(class) {
|
||||
self.draw_runs(pass, self.class(class), tally);
|
||||
}
|
||||
|
||||
/// Draw the closed bodies' opaque triangles ([`BlockBytes::closed`]), as [`BlockGpu::draw`].
|
||||
fn draw_closed(&self, pass: &mut wgpu::RenderPass<'_>, tally: &Tally) {
|
||||
self.draw_runs(pass, self.closed.as_ref(), tally);
|
||||
}
|
||||
|
||||
fn draw_runs(&self, pass: &mut wgpu::RenderPass<'_>, runs: Option<&ClassGpu>, tally: &Tally) {
|
||||
match runs {
|
||||
Some(ClassGpu::Runs(runs)) => {
|
||||
let Some(vertices) = &self.vertices else { return };
|
||||
pass.set_vertex_buffer(0, vertices.slice(..));
|
||||
@@ -1485,6 +1541,8 @@ struct Batches {
|
||||
block_order: Vec<u64>,
|
||||
key: Option<u64>,
|
||||
tris: Option<Batch>,
|
||||
/// [`GpuScene::open_tris`].
|
||||
open_tris: Option<Batch>,
|
||||
pbr: Option<Batch>,
|
||||
trans: Option<Batch>,
|
||||
shadow: Option<Batch>,
|
||||
@@ -1576,6 +1634,7 @@ impl Batches {
|
||||
upload(device, label, data, stride)
|
||||
};
|
||||
self.tris = own("sc_tris", &sc.tris, TRI_SIZE);
|
||||
self.open_tris = own("sc_open_tris", &sc.open_tris, TRI_SIZE);
|
||||
self.pbr = own("sc_pbr", &sc.pbr, PBR_SIZE);
|
||||
self.trans = own("sc_trans", &sc.trans, TRI_SIZE);
|
||||
self.shadow = own("sc_shadow", &sc.shadow, TRI_SIZE);
|
||||
@@ -1658,7 +1717,7 @@ impl Batches {
|
||||
self.instanced_tris.insert(key, gpu);
|
||||
}
|
||||
if !levels.is_empty() {
|
||||
out.push(InstancedDraw { look: m.look, levels, bounds: m.tris.bounds, placements, shown: None });
|
||||
out.push(InstancedDraw { look: m.look, closed: m.closed, levels, bounds: m.tris.bounds, placements, shown: None });
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -1741,7 +1800,9 @@ impl Batches {
|
||||
for b in bodies {
|
||||
let n = b.plan.vertex_count();
|
||||
match chunks.last_mut() {
|
||||
Some(c) if open && filled + n <= crate::MAX_BATCH && c.first().is_some_and(|f| f.class == b.class) => c.push(b),
|
||||
Some(c) if open && filled + n <= crate::MAX_BATCH && c.first().is_some_and(|f| (f.class, f.closed) == (b.class, b.closed)) => {
|
||||
c.push(b)
|
||||
}
|
||||
_ => {
|
||||
chunks.push(vec![b]);
|
||||
filled = 0;
|
||||
@@ -1785,8 +1846,8 @@ impl Batches {
|
||||
}
|
||||
self.uploads.planned += bytes.len() as u64;
|
||||
if let Some((colors, _)) = upload(device, "sc_planned_colors", &bytes, 4).and_then(|c| c.parts.into_iter().next()) {
|
||||
let class = chunk.first().map_or(TriClass::Opaque, |b| b.class);
|
||||
out.push(PlannedDraw { mesh, colors, class, plans: key(chunk) });
|
||||
let (class, closed) = chunk.first().map_or((TriClass::Opaque, true), |b| (b.class, b.closed));
|
||||
out.push(PlannedDraw { mesh, colors, class, closed, plans: key(chunk) });
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -3134,8 +3195,9 @@ impl Renderer {
|
||||
pass.set_bind_group(1, &res.access_bind, &[]);
|
||||
pass.set_bind_group(2, &res.ao.bind, &[]);
|
||||
pass.set_bind_group(0, scene_bind(m, &res.bind), &[]);
|
||||
let planned = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline| {
|
||||
for d in m.planned.iter().filter(|d| d.class == TriClass::Opaque) {
|
||||
// `counting`: only closed bodies, for a section's crossing count ([`Cut::count`]).
|
||||
let planned = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline, counting: bool| {
|
||||
for d in m.planned.iter().filter(|d| d.class == TriClass::Opaque && (d.closed || !counting)) {
|
||||
pass.set_pipeline(p);
|
||||
pass.set_vertex_buffer(0, d.mesh.vertices.slice(..));
|
||||
pass.set_vertex_buffer(1, d.colors.slice(..));
|
||||
@@ -3145,8 +3207,8 @@ impl Renderer {
|
||||
}
|
||||
};
|
||||
// Instanced meshes, with the pipeline for smooth and for flat triangles.
|
||||
let instanced = |pass: &mut wgpu::RenderPass<'static>, smooth: &wgpu::RenderPipeline, flat: &wgpu::RenderPipeline| {
|
||||
for d in m.instanced.iter().filter(|d| d.look == InstanceLook::Opaque) {
|
||||
let instanced = |pass: &mut wgpu::RenderPass<'static>, smooth: &wgpu::RenderPipeline, flat: &wgpu::RenderPipeline, counting: bool| {
|
||||
for d in m.instanced.iter().filter(|d| d.look == InstanceLook::Opaque && (d.closed || !counting)) {
|
||||
d.draw(pass, smooth, flat, tally);
|
||||
}
|
||||
};
|
||||
@@ -3198,32 +3260,46 @@ impl Renderer {
|
||||
(Some(cut), Some(cut_planned), Some(cut_instanced), Some(cut_flat), Some(base)) => {
|
||||
// The model's own triangles, blocks, plans and instances with its palette, then the
|
||||
// preview's with its own.
|
||||
let all = |pass: &mut wgpu::RenderPass<'static>, pick: fn(&Cut) -> &wgpu::RenderPipeline| {
|
||||
// `counting`: the crossing count, which leaves open bodies out.
|
||||
let all = |pass: &mut wgpu::RenderPass<'static>, pick: fn(&Cut) -> &wgpu::RenderPipeline, counting: bool| {
|
||||
pass.set_bind_group(0, scene_bind(m, &res.bind), &[]);
|
||||
tris(pass, pick(cut), &m.tris);
|
||||
block_class(pass, pick(cut), TriClass::Opaque);
|
||||
planned(pass, pick(cut_planned));
|
||||
instanced(pass, pick(cut_instanced), pick(cut_flat));
|
||||
if counting {
|
||||
for block in m.block_order.iter().filter_map(|k| m.blocks.get(k)).filter(|b| b.closed.is_some()) {
|
||||
pass.set_pipeline(pick(cut));
|
||||
block.draw_closed(pass, tally);
|
||||
}
|
||||
} else {
|
||||
tris(pass, pick(cut), &m.open_tris);
|
||||
block_class(pass, pick(cut), TriClass::Opaque);
|
||||
}
|
||||
planned(pass, pick(cut_planned), counting);
|
||||
instanced(pass, pick(cut_instanced), pick(cut_flat), counting);
|
||||
pass.set_bind_group(0, scene_bind(pv, &res.bind), &[]);
|
||||
tris(pass, pick(cut), &pv.tris);
|
||||
if !counting {
|
||||
tris(pass, pick(cut), &pv.open_tris);
|
||||
}
|
||||
};
|
||||
pass.set_stencil_reference(STENCIL_BASE);
|
||||
pass.set_pipeline(base);
|
||||
pass.draw(0..3, 0..1);
|
||||
tally.call();
|
||||
all(pass, |c| &c.count);
|
||||
all(pass, |c| &c.fill);
|
||||
all(pass, |c| &c.cap);
|
||||
all(pass, |c| &c.rest);
|
||||
all(pass, |c| &c.count, true);
|
||||
all(pass, |c| &c.fill, false);
|
||||
all(pass, |c| &c.cap, false);
|
||||
all(pass, |c| &c.rest, false);
|
||||
}
|
||||
_ => {
|
||||
pass.set_bind_group(0, scene_bind(m, &res.bind), &[]);
|
||||
tris(pass, &res.tri, &m.tris);
|
||||
tris(pass, &res.tri, &m.open_tris);
|
||||
block_class(pass, &res.tri, TriClass::Opaque);
|
||||
planned(pass, &res.tri_planned);
|
||||
instanced(pass, &res.tri_instanced, &res.tri_instanced_flat);
|
||||
planned(pass, &res.tri_planned, false);
|
||||
instanced(pass, &res.tri_instanced, &res.tri_instanced_flat, false);
|
||||
pass.set_bind_group(0, scene_bind(pv, &res.bind), &[]);
|
||||
tris(pass, &res.tri, &pv.tris);
|
||||
tris(pass, &res.tri, &pv.open_tris);
|
||||
}
|
||||
}
|
||||
pass.set_bind_group(0, scene_bind(m, &res.bind), &[]);
|
||||
|
||||
@@ -155,7 +155,7 @@ impl Resources {
|
||||
for (batch, bind) in batches.into_iter().zip(binds) {
|
||||
pass.set_bind_group(0, bind, &[]);
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
if let Some(tris) = &batch.tris {
|
||||
for tris in [&batch.tris, &batch.open_tris].into_iter().flatten() {
|
||||
for (buffer, count) in &tris.parts {
|
||||
pass.set_vertex_buffer(0, buffer.slice(..));
|
||||
pass.draw(0..*count, 0..1);
|
||||
|
||||
+36
-26
@@ -291,42 +291,52 @@ fn an_edge_on_the_cut_face_draws_over_the_cap() {
|
||||
/// lies on a larger plate's front face, cut at y = 15: through the cut the block's far wall and
|
||||
/// the plate's face are at one depth. When the cap went to the nearer of the two by depth, the
|
||||
/// plate showed through 2721 of its 4350 px here (an engine's sleeves in their bores, pins in
|
||||
/// their bosses); counted by crossings, the cap is the same with the plate as without it.
|
||||
/// their bosses); counted by crossings, the cap is the same with the plate as without it. An
|
||||
/// open body behind it (a box without its far face, whose near face alone the ray crosses)
|
||||
/// takes nothing from the cap either: only closed bodies are counted.
|
||||
#[test]
|
||||
fn a_cap_covers_the_body_its_cut_body_touches() {
|
||||
let block = ("solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "block"}));
|
||||
let plate = ("solid.box", json!({"length": 70, "width": 10, "height": 50, "corner": [-15, 30, -15], "operation": "new", "body_name": "plate"}));
|
||||
let shell = ("solid.box", json!({"length": 70, "width": 10, "height": 50, "corner": [-15, 35, -15], "operation": "new", "body_name": "shell"}));
|
||||
let far_face = ("face.delete", json!({"faces": [[20, 45, 10]], "heal": false}));
|
||||
let cut = ("inspect.section", json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}}));
|
||||
let alone = session(&[block.clone(), cut.clone()]);
|
||||
let touching = session(&[block, plate, cut]);
|
||||
let touching = session(&[block.clone(), plate, cut.clone()]);
|
||||
let open = session(&[block, shell, far_face, cut]);
|
||||
assert!(open.world_state().unwrap().body("shell").unwrap().body.is_surface());
|
||||
let (w, h) = (360, 280);
|
||||
for (mode, c) in capturers() {
|
||||
for fov in [0.0, 0.6] {
|
||||
let mut cam = view::home_camera(&touching).unwrap();
|
||||
(cam.fov, cam.yaw, cam.pitch) = (fov, -0.3, 0.2);
|
||||
let radius = view::scene(&touching, &cam).unwrap().radius;
|
||||
let shot = |s: &Session| c.rgba(&Scene { grid: Vec::new(), lines: Vec::new(), radius, ..view::scene(s, &cam).unwrap() }, &cam, w, h).unwrap();
|
||||
let (a, b) = (shot(&alone), shot(&touching));
|
||||
// The cap on screen: the cut's rectangle (x 0..40, z 0..20 at y = 15), 2 px in.
|
||||
let corner = |x: f64, z: f64| cam.to_screen(Vec3::new(x, 15.0, z), f64::from(w), f64::from(h), radius).map(|(x, y, _)| (x, y));
|
||||
let cap: Vec<(f64, f64)> = [(0.0, 0.0), (40.0, 0.0), (40.0, 20.0), (0.0, 20.0)].iter().filter_map(|(x, z)| corner(*x, *z)).collect();
|
||||
assert_eq!(cap.len(), 4, "fov {fov}: the cut is on screen");
|
||||
let turn = (0..4).map(|i| cap[i].0 * cap[(i + 1) % 4].1 - cap[(i + 1) % 4].0 * cap[i].1).sum::<f64>().signum();
|
||||
let inside = |x: f64, y: f64| {
|
||||
(0..4).all(|i| {
|
||||
let ((ax, ay), (bx, by)) = (cap[i], cap[(i + 1) % 4]);
|
||||
((bx - ax) * (y - ay) - (by - ay) * (x - ax)) / (bx - ax).hypot(by - ay) * turn >= 2.0
|
||||
})
|
||||
};
|
||||
let (mut shown, mut all) = (0, 0);
|
||||
for (i, (p, q)) in pixels(&a).zip(pixels(&b)).enumerate() {
|
||||
if inside((i % w as usize) as f64 + 0.5, (i / w as usize) as f64 + 0.5) {
|
||||
all += 1;
|
||||
shown += usize::from(p.iter().zip(q).any(|(u, v)| u.abs_diff(v) > 24));
|
||||
for (what, other) in [("the touching plate", &touching), ("an open shell behind", &open)] {
|
||||
for fov in [0.0, 0.6] {
|
||||
let mut cam = view::home_camera(&touching).unwrap();
|
||||
(cam.fov, cam.yaw, cam.pitch) = (fov, -0.3, 0.2);
|
||||
let radius = view::scene(&touching, &cam).unwrap().radius;
|
||||
let shot = |s: &Session| {
|
||||
c.rgba(&Scene { grid: Vec::new(), lines: Vec::new(), radius, ..view::scene(s, &cam).unwrap() }, &cam, w, h).unwrap()
|
||||
};
|
||||
let (a, b) = (shot(&alone), shot(other));
|
||||
// The cap on screen: the cut's rectangle (x 0..40, z 0..20 at y = 15), 2 px in.
|
||||
let corner = |x: f64, z: f64| cam.to_screen(Vec3::new(x, 15.0, z), f64::from(w), f64::from(h), radius).map(|(x, y, _)| (x, y));
|
||||
let cap: Vec<(f64, f64)> = [(0.0, 0.0), (40.0, 0.0), (40.0, 20.0), (0.0, 20.0)].iter().filter_map(|(x, z)| corner(*x, *z)).collect();
|
||||
assert_eq!(cap.len(), 4, "fov {fov}: the cut is on screen");
|
||||
let turn = (0..4).map(|i| cap[i].0 * cap[(i + 1) % 4].1 - cap[(i + 1) % 4].0 * cap[i].1).sum::<f64>().signum();
|
||||
let inside = |x: f64, y: f64| {
|
||||
(0..4).all(|i| {
|
||||
let ((ax, ay), (bx, by)) = (cap[i], cap[(i + 1) % 4]);
|
||||
((bx - ax) * (y - ay) - (by - ay) * (x - ax)) / (bx - ax).hypot(by - ay) * turn >= 2.0
|
||||
})
|
||||
};
|
||||
let (mut shown, mut all) = (0, 0);
|
||||
for (i, (p, q)) in pixels(&a).zip(pixels(&b)).enumerate() {
|
||||
if inside((i % w as usize) as f64 + 0.5, (i / w as usize) as f64 + 0.5) {
|
||||
all += 1;
|
||||
shown += usize::from(p.iter().zip(q).any(|(u, v)| u.abs_diff(v) > 24));
|
||||
}
|
||||
}
|
||||
assert!(all > 3000, "{mode}, fov {fov}: the cap ({all} px)");
|
||||
assert_eq!(shown, 0, "{mode} depth, fov {fov}: {what} shows through {shown} of {all} cap px");
|
||||
}
|
||||
assert!(all > 3000, "{mode}, fov {fov}: the cap ({all} px)");
|
||||
assert_eq!(shown, 0, "{mode} depth, fov {fov}: the touching plate shows through {shown} of {all} cap px");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ mod tests {
|
||||
|
||||
/// A front panel over a back one, with a line between them, seen from the front.
|
||||
fn two_panels(front_opacity: f32) -> (Scene, Camera) {
|
||||
let glass = SceneMesh { mesh: Arc::new(panel(-5.0)), color: Rgb(255, 0, 0), face_colors: Vec::new(), opacity: front_opacity };
|
||||
let glass = SceneMesh { mesh: Arc::new(panel(-5.0)), color: Rgb(255, 0, 0), face_colors: Vec::new(), opacity: front_opacity, closed: true };
|
||||
let scene = Scene {
|
||||
meshes: vec![SceneMesh::solid(Arc::new(panel(5.0)), Rgb(0, 255, 0)), glass],
|
||||
lines: vec![SceneLine {
|
||||
|
||||
@@ -27,12 +27,15 @@ pub struct SceneMesh {
|
||||
/// drawn see-through, so what lies inside it shows, as the viewport draws glass. A body
|
||||
/// without an appearance of its own is solid.
|
||||
pub opacity: f32,
|
||||
/// The body encloses a volume (a solid, a closed mesh); an open one (a surface body, a solid
|
||||
/// with faces left out) has no inside, so a section gives it no cap.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl SceneMesh {
|
||||
/// A solid body in `color`, with no face of its own look.
|
||||
pub fn solid(mesh: Arc<Mesh>, color: Rgb) -> SceneMesh {
|
||||
SceneMesh { mesh, color, face_colors: Vec::new(), opacity: 1.0 }
|
||||
SceneMesh { mesh, color, face_colors: Vec::new(), opacity: 1.0, closed: true }
|
||||
}
|
||||
|
||||
/// Is the body drawn see-through? (Its opacity is below [`SEE_THROUGH`].)
|
||||
|
||||
@@ -920,6 +920,9 @@ struct BodyDraw {
|
||||
/// The render class of each face (opaque, glass or faded).
|
||||
face_classes: Vec<crate::gpu::TriClass>,
|
||||
faded: bool,
|
||||
/// The body encloses a volume: only closed bodies take part in a section's crossing count
|
||||
/// ([`crate::gpu::BlockBytes::closed`]).
|
||||
closed: bool,
|
||||
/// Its faces are drawn elsewhere, from its plan on the GPU or as an instance of shared
|
||||
/// triangles (only its edges are built here).
|
||||
on_gpu: bool,
|
||||
@@ -936,7 +939,7 @@ impl BodyDraw {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(std::sync::Arc::as_ptr(&self.mesh) as usize).hash(&mut h);
|
||||
self.place.iter().flatten().flatten().for_each(|v| v.to_bits().hash(&mut h));
|
||||
(self.faded, self.on_gpu, shaded, edges, hidden_edges, self.slot_base, self.edge.0, self.hidden_edge.0).hash(&mut h);
|
||||
(self.faded, self.closed, self.on_gpu, shaded, edges, hidden_edges, self.slot_base, self.edge.0, self.hidden_edge.0).hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
@@ -949,6 +952,7 @@ impl BodyDraw {
|
||||
}
|
||||
m.tri_face.get(ti).and_then(|f| self.face_classes.get(*f as usize)).copied().unwrap_or(crate::gpu::TriClass::Opaque)
|
||||
};
|
||||
out.open_body(!self.closed);
|
||||
if shaded && !self.on_gpu {
|
||||
let corner = |k: u32| {
|
||||
let i = k as usize;
|
||||
@@ -1055,6 +1059,7 @@ fn build_block(draws: &[BodyDraw], shaded: bool, build: &impl Fn(&BodyDraw, &mut
|
||||
fn reindex_block(block: CachedBlock, draws: &[BodyDraw]) -> CachedBlock {
|
||||
use crate::gpu::TriClass;
|
||||
let mut runs: [Vec<std::ops::Range<u32>>; 3] = Default::default();
|
||||
let mut closed = Vec::new();
|
||||
for (range, d) in block.ranges.iter().zip(draws) {
|
||||
// The triangles this body wrote (none when its faces are drawn from a plan or as an
|
||||
// instance), never more than its mesh has.
|
||||
@@ -1070,6 +1075,9 @@ fn reindex_block(block: CachedBlock, draws: &[BodyDraw]) -> CachedBlock {
|
||||
if let Some(list) = runs.get_mut(slot) {
|
||||
crate::gpu::extend_runs(list, at);
|
||||
}
|
||||
if class == TriClass::Opaque && d.closed {
|
||||
crate::gpu::extend_runs(&mut closed, at);
|
||||
}
|
||||
}
|
||||
}
|
||||
let [opaque, glass, faded] = runs;
|
||||
@@ -1088,6 +1096,7 @@ fn reindex_block(block: CachedBlock, draws: &[BodyDraw]) -> CachedBlock {
|
||||
let bytes = std::sync::Arc::new(crate::gpu::BlockBytes {
|
||||
vertices: old.vertices.clone(),
|
||||
opaque: std::sync::Arc::new(opaque),
|
||||
closed: std::sync::Arc::new(closed),
|
||||
glass: std::sync::Arc::new(glass),
|
||||
faded: std::sync::Arc::new(faded),
|
||||
lines: old.lines.clone(),
|
||||
@@ -1156,7 +1165,10 @@ impl Instances {
|
||||
self.lods.levels(&mesh).map(|l| l.as_ref().clone()).unwrap_or_default(),
|
||||
),
|
||||
};
|
||||
self.meshes.push((mesh.clone(), crate::gpu::InstancedMesh { key: key as u64, tris, instances: Vec::new(), look, levels }));
|
||||
self.meshes.push((
|
||||
mesh.clone(),
|
||||
crate::gpu::InstancedMesh { key: key as u64, tris, instances: Vec::new(), look, levels, closed: b.is_closed() },
|
||||
));
|
||||
self.by_mesh.insert((key, look), self.meshes.len() - 1);
|
||||
self.meshes.len() - 1
|
||||
}
|
||||
@@ -1303,11 +1315,12 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
face_colors.push(slot);
|
||||
face_classes.push(class);
|
||||
}
|
||||
let closed = b.is_closed();
|
||||
let class = plan_class(app, &face_classes);
|
||||
let planned = class.and_then(|class| drawn_plan(app, b, true).map(|plan| (plan, class)));
|
||||
let on_gpu = planned.is_some();
|
||||
if let Some((plan, class)) = planned {
|
||||
sc.planned.push(crate::gpu::PlannedBody { plan, face_colors: face_colors.clone(), class });
|
||||
sc.planned.push(crate::gpu::PlannedBody { plan, face_colors: face_colors.clone(), class, closed });
|
||||
}
|
||||
// Opaque, faded and see-through bodies alike, in one colour (faces of their own colour
|
||||
// are built as before).
|
||||
@@ -1335,6 +1348,7 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
body_color: slots.body,
|
||||
face_classes,
|
||||
faded,
|
||||
closed,
|
||||
on_gpu: true,
|
||||
slot_base: slots.faces,
|
||||
edge: slots.edge,
|
||||
@@ -1348,6 +1362,7 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
body_color: slots.body,
|
||||
face_classes,
|
||||
faded,
|
||||
closed,
|
||||
on_gpu,
|
||||
slot_base: slots.faces,
|
||||
edge: slots.edge,
|
||||
@@ -3127,6 +3142,7 @@ impl HighlightInstances {
|
||||
instances: Vec::new(),
|
||||
look: crate::gpu::InstanceLook::Opaque,
|
||||
levels: Vec::new(),
|
||||
closed: true,
|
||||
});
|
||||
self.by_mesh.insert(key, self.meshes.len() - 1);
|
||||
self.meshes.len() - 1
|
||||
|
||||
Reference in New Issue
Block a user