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:
jyviko
2026-10-28 15:15:13 -04:00
committed by Irakli Kourtis
parent 33acb6dc39
commit c33bae8701
10 changed files with 182 additions and 62 deletions
+17 -4
View File
@@ -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),
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+5 -3
View File
@@ -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
View File
@@ -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), &[]);
+1 -1
View File
@@ -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
View File
@@ -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");
}
}
}
+1 -1
View File
@@ -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 {
+4 -1
View File
@@ -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`].)
+19 -3
View File
@@ -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