From c184535d5e4309e642de4146639b60b4ba3c8106 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 6 Apr 2026 23:44:36 +0300 Subject: [PATCH] fix: render wipeout fills after wire pass so they correctly mask wires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously wipeout solid fills were part of the hatch pass (pass 1), drawn before wires (pass 5), so all wires appeared on top of the wipeout. This fix moves wipeout fills into a dedicated pass 6 that runs after wires, so the background-colored polygon correctly covers the lines/text beneath it — matching AutoCAD behaviour. - Scene.wipeout_models() separated from synced_hatch_models() - Primitive.wipeout_hatches populated via wipeout_models() - Pipeline.gpu_wipeouts + upload_wipeouts() + pass 6 after wires Co-Authored-By: Claude Sonnet 4.6 --- src/scene/mod.rs | 12 +++++---- src/scene/pipeline/mod.rs | 52 +++++++++++++++++++++++++++++++++++++++ src/scene/render.rs | 4 +++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bf04f246b..18e40c7cd 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1249,15 +1249,18 @@ impl Scene { } } - // ── Wipeout solid fills ─────────────────────────────────────────────── - // Wipeouts are rendered as background-colored filled polygons so that - // they mask hatch fills drawn earlier in the same pass. - // Wipeout fills are appended last so they render on top of other hatches. + models + } + + /// Wipeout fill models — rendered in a separate pass AFTER wires so that + /// wipeouts correctly mask everything below them in the draw order. + pub(super) fn wipeout_models(&self) -> Vec { let bg_color: [f32; 4] = if self.current_layout == "Model" { self.bg_color } else { self.paper_bg_color }; + let mut models = Vec::new(); for entity in self.document.entities() { let EntityType::Wipeout(wo) = entity else { continue }; if entity.common().invisible { @@ -1286,7 +1289,6 @@ impl Scene { }); } } - models } diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 020d02dfa..abdc007e4 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -33,6 +33,8 @@ pub struct Pipeline { depth_view: wgpu::TextureView, gpu_wires: Vec, gpu_hatches: Vec, + /// Wipeout fills — rendered after wires in a separate pass. + gpu_wipeouts: Vec, gpu_images: Vec, gpu_meshes: Vec, pub viewcube: ViewCubePipeline, @@ -356,6 +358,7 @@ impl Pipeline { depth_view, gpu_wires: vec![], gpu_hatches: vec![], + gpu_wipeouts: vec![], gpu_images: vec![], gpu_meshes: vec![], viewcube, @@ -382,6 +385,14 @@ impl Pipeline { .collect(); } + pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) { + self.gpu_wipeouts = wipeouts + .iter() + .filter(|h| h.boundary.len() >= 3) + .map(|h| HatchGpu::new(device, h, &self.hatch_bgl1)) + .collect(); + } + pub fn upload_images( &mut self, device: &wgpu::Device, @@ -574,6 +585,47 @@ impl Pipeline { } } } + + // ── Pass 6: wipeout fills (drawn after wires to mask them) ──────── + if !self.gpu_wipeouts.is_empty() { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("wipeout.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_viewport( + vp.x as f32, + vp.y as f32, + vp.width as f32, + vp.height as f32, + 0.0, + 1.0, + ); + pass.set_pipeline(&self.hatch_pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + for wipeout in &self.gpu_wipeouts { + pass.set_bind_group(1, &wipeout.bind_group, &[]); + pass.set_vertex_buffer(0, wipeout.vertex_buffer.slice(..)); + pass.draw(0..6, 0..1); + } + } } pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size) { diff --git a/src/scene/render.rs b/src/scene/render.rs index 23e3b85f0..453255180 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -27,6 +27,8 @@ pub struct CameraState { pub struct Primitive { pub(super) wires: Vec, pub(super) hatches: Vec, + /// Wipeout fills — rendered in a separate pass AFTER wires. + pub(super) wipeout_hatches: Vec, pub(super) images: Vec, pub(super) meshes: Vec, pub(super) uniforms: Uniforms, @@ -60,6 +62,7 @@ impl shader::Program for Scene { Primitive { wires: all_wires, hatches: self.synced_hatch_models(), + wipeout_hatches: self.wipeout_models(), images: self.images.values().cloned().collect(), meshes: self.meshes.values().cloned().collect(), uniforms: Uniforms::new(&cam, bounds), @@ -125,6 +128,7 @@ impl shader::Primitive for Primitive { pipeline.viewcube.ensure_depth_texture(device, size); pipeline.upload_uniforms(queue, &self.uniforms); pipeline.upload_hatches(device, &self.hatches); + pipeline.upload_wipeouts(device, &self.wipeout_hatches); pipeline.upload_images(device, queue, &self.images); pipeline.upload_meshes(device, &self.meshes); pipeline.upload_wires(device, &self.wires);