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,