From aec9a9575e86ca88f492dd48e6c9a6df446b6513 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 3 Jun 2026 00:36:28 +0300 Subject: [PATCH] fix(paper): keep floating-viewport fills clipped + show lines while editing in MSPACE Two paper-space rendering fixes: - The full-canvas sheet instance no longer draws model-block hatches/ wipeouts/images. Those belong inside the floating content viewports; rendering them on the sheet let them bleed past the viewport borders when model coords overlapped the paper area. The sheet now draws only the paper block's own fills plus the synthetic white printable area. - Pan/zoom/orbit inside a double-clicked floating viewport (MSPACE) now re-culls and re-uploads the viewport's wire set. The per-viewport wire cache key now hashes the viewport's own view (pan + zoom + orbit), and the active-viewport pan/zoom/orbit handlers bump camera_generation so the GPU re-uploads. Previously the stale frustum-culled subset stayed on the GPU, so newly-revealed lines were invisible (hatches, uploaded as the full set, still showed) until MSPACE was exited. Co-Authored-By: Claude Opus 4.8 --- src/app/update.rs | 10 +++++++++ src/scene/mod.rs | 49 +++++++++++++++++++++++++++++++++++++++++++-- src/scene/render.rs | 26 +++++++++++++++--------- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/app/update.rs b/src/app/update.rs index 28ed6a15e..a467f4299 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1950,6 +1950,9 @@ impl OpenCADStudio { sel.right_last_pos = Some(p); drop(sel); self.tabs[i].scene.orbit_active_viewport(dx, dy); + // Bump so the GPU re-uploads the viewport's + // re-culled wire set after the view rotates. + self.tabs[i].scene.camera_generation += 1; return Task::none(); } else if self.tabs[i].scene.current_layout == "Model" { sel.right_last_pos = Some(p); @@ -1986,6 +1989,10 @@ impl OpenCADStudio { drop(sel); if self.tabs[i].scene.active_viewport.is_some() { self.tabs[i].scene.pan_active_viewport(dx, dy, bounds); + // Bump so the GPU re-uploads the viewport's re-culled + // wire set — otherwise newly-revealed lines stay + // invisible until MSPACE is exited. + self.tabs[i].scene.camera_generation += 1; } else { // `bounds` is the active tile; pan by its height so // the point under the cursor tracks correctly. @@ -3169,6 +3176,9 @@ impl OpenCADStudio { glam::Vec2::new(pt.x, pt.y) }); self.tabs[i].scene.zoom_active_viewport(s, cursor_paper); + // Bump so the GPU re-uploads the viewport's re-culled wire + // set after zooming inside it. + self.tabs[i].scene.camera_generation += 1; } else { // Model space: zoom about the cursor within the active // tile so the point under it stays put in that pane. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 2e3f7bdb6..24d679032 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -598,7 +598,7 @@ pub struct Scene { mesh_cache: RefCell>)>>, /// Per-viewport wire cache for paper-space rendering. /// Maps vp_handle → (geometry_epoch, Arc>). - viewport_wire_cache: RefCell>)>>, + viewport_wire_cache: RefCell>)>>, /// Cached tessellation of paper-space layout block entities (title block, annotations, etc.). /// Separate from `wire_cache` so the GPU sheet viewport doesn't re-tessellate every frame. /// Keyed by `(geometry_epoch, camera_generation)` — paper view changes @@ -1621,6 +1621,31 @@ impl Scene { arc } + /// Images owned by the active paper layout block only. The full-canvas + /// sheet viewport uses this so model-block images don't bleed onto the + /// paper sheet (mirrors `paper_canvas_hatches`). + pub(super) fn paper_sheet_images(&self) -> Arc> { + let layout_block = self.current_layout_block_handle(); + let depth_map = self.draw_depth_map(); + Arc::new( + self.images + .iter() + .filter_map(|(&handle, model)| { + let entity = self.document.get_entity(handle)?; + let c = entity.common(); + if c.invisible + || !self.belongs_to_visible_block(handle, c.owner_handle, layout_block) + { + return None; + } + let mut m = model.clone(); + m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0); + Some(m) + }) + .collect(), + ) + } + pub(super) fn meshes_arc(&self) -> Arc> { { let cache = self.mesh_cache.borrow(); @@ -6119,7 +6144,27 @@ impl Scene { // re-tessellate a 100k-entity drawing every frame; round to an // integer pixel. let height_key = screen_height_px.max(1.0).round() as u32; - let key = (self.geometry_epoch, height_key); + // Hash the viewport's own view (pan + zoom + orbit) into the key. + // Editing inside the viewport (MSPACE) changes its frustum but does NOT + // bump geometry_epoch, so without this the stale frustum-culled subset + // is returned and newly-revealed lines stay invisible until the layout + // re-tessellates. Quantize to ~1 px / fine steps to ignore jitter. + let view_key = { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + if let Some(EntityType::Viewport(vp)) = self.document.get_entity(vp_handle) { + let vh = vp.view_height.abs().max(1e-6); + let q = vh / (screen_height_px.max(1.0) as f64); // model units / px + (((vp.view_target.x + vp.view_center.x) / q).round() as i64).hash(&mut h); + (((vp.view_target.y + vp.view_center.y) / q).round() as i64).hash(&mut h); + ((vh * 1000.0).round() as i64).hash(&mut h); + ((vp.view_direction.x * 1000.0).round() as i64).hash(&mut h); + ((vp.view_direction.y * 1000.0).round() as i64).hash(&mut h); + ((vp.view_direction.z * 1000.0).round() as i64).hash(&mut h); + } + h.finish() + }; + let key = (self.geometry_epoch, height_key, view_key); { let cache = self.viewport_wire_cache.borrow(); if let Some((cached_key, ref arc)) = cache.get(&vp_handle) { diff --git a/src/scene/render.rs b/src/scene/render.rs index 831a913e9..e31bcd3c9 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -650,22 +650,28 @@ impl Scene { height: visible_h / canvas.1, }; - // The paper sheet instance renders the same hatch / wipeout / image - // fills as everything else (wrong-camera fills fall outside its - // frustum and are culled), plus a synthetic white fill for the sheet's - // printable area — the GPU equivalent of the old PaperCanvas chrome. - let hatches = if inst.paper_sheet { + // The paper sheet instance renders only the paper layout block's own + // fills (plus a synthetic white fill for the printable area) — NOT the + // model-block hatches. Those belong inside the floating content + // viewports; rendering them on the full-canvas sheet would let them + // bleed past the viewport borders whenever model coords overlap the + // paper area. Content viewports keep the full set (the model camera + + // per-viewport scissor place / clip them correctly). + let (hatches, wipeout_hatches) = if inst.paper_sheet { let mut v: Vec = Vec::new(); if let Some(sheet) = self.paper_sheet_fill() { v.push(sheet); } - v.extend(self.hatch_models_arc().iter().cloned()); - Arc::new(v) + v.extend(self.paper_canvas_hatches().iter().cloned()); + (Arc::new(v), self.paper_canvas_wipeouts()) } else { - self.hatch_models_arc() + (self.hatch_models_arc(), self.wipeout_models_arc()) + }; + let images = if inst.paper_sheet { + self.paper_sheet_images() + } else { + self.images_arc() }; - let wipeout_hatches = self.wipeout_models_arc(); - let images = self.images_arc(); Some(ViewportData { wires: all_wires,