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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-03 00:36:28 +03:00
1 parent b7230baae1
commit aec9a9575e
3 files changed
+73 -12

No files matched your search

+10
View File
@@ -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.
+47 -2
View File
@@ -598,7 +598,7 @@ pub struct Scene {
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshLodSet>>)>>,
/// Per-viewport wire cache for paper-space rendering.
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>).
viewport_wire_cache: RefCell<HashMap<Handle, ((u64, u32), Arc<Vec<WireModel>>)>>,
viewport_wire_cache: RefCell<HashMap<Handle, ((u64, u32, u64), Arc<Vec<WireModel>>)>>,
/// 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<Vec<ImageModel>> {
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<Vec<MeshLodSet>> {
{
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) {
+16 -10
View File
@@ -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<HatchModel> = 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,