refactor(paper): drop sheet viewport border + clean stale PaperCanvas docs

The overall "sheet" viewport now IS the paper view, so its own border
rectangle must not be drawn as an entity — filter it out of
paper_sheet_wires_arc (content viewport borders are kept).

Also update the doc comments that still referenced the removed
PaperCanvas widget / paper_canvas_wires to describe the GPU sheet path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-02 20:33:51 +03:00
commit ac4126072c
3 changed files with 38 additions and 36 deletions

View file

@ -248,15 +248,14 @@ impl OpenCADStudio {
let i = self.active_tab; let i = self.active_tab;
let tab = &self.tabs[i]; let tab = &self.tabs[i];
let is_paper = tab.scene.current_layout != "Model"; let is_paper = tab.scene.current_layout != "Model";
// Start tab: render welcome page in place of the model/paper canvas. // Start tab: render welcome page in place of the viewport.
// Surrounding chrome (tab bar, status bar) stays; the welcome widget // Surrounding chrome (tab bar, status bar) stays; the welcome widget
// returned here also flags the rest of `view` to skip drawing-only // returned here also flags the rest of `view` to skip drawing-only
// overlays via `tab.is_start`. // overlays via `tab.is_start`.
// Unified GPU widget for both layouts. In a paper layout the // Unified GPU widget for both layouts. A paper layout renders through
// PaperCanvas (2-D sheet + paper entities + viewport borders) is // the same shader as model space: a full-canvas top-locked "sheet"
// layered underneath this shader so the area outside floating // viewport draws the layout's own geometry (white sheet + entities +
// viewports shows the sheet; the shader only blits inside each // borders) and the floating content viewports blit on top.
// content viewport's scissor rect.
let viewport_3d: Element<'_, Message> = if tab.is_start { let viewport_3d: Element<'_, Message> = if tab.is_start {
start_page_view() start_page_view()
} else { } else {
@ -576,7 +575,7 @@ impl OpenCADStudio {
// Active paper-space viewport overlays: a render-mode picker in // Active paper-space viewport overlays: a render-mode picker in
// its top-left corner and a ViewCube hit area in its top-right, // its top-left corner and a ViewCube hit area in its top-right,
// both layered ABOVE the viewport mouse_area so they receive // both layered ABOVE the viewport mouse_area so they receive
// clicks (the paper canvas itself sits below it). Positioned with // clicks (the shader viewport sits below it). Positioned with
// leading Spaces sized to the viewport's screen rectangle. // leading Spaces sized to the viewport's screen rectangle.
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start { let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
tab.scene.active_viewport.and_then(|h| { tab.scene.active_viewport.and_then(|h| {

View file

@ -600,11 +600,11 @@ pub struct Scene {
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>). /// 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), Arc<Vec<WireModel>>)>>,
/// Cached tessellation of paper-space layout block entities (title block, annotations, etc.). /// Cached tessellation of paper-space layout block entities (title block, annotations, etc.).
/// Separate from `wire_cache` so paper_canvas_wires() doesn't re-tessellate on every frame. /// Separate from `wire_cache` so the GPU sheet viewport doesn't re-tessellate every frame.
/// Keyed by `(geometry_epoch, camera_generation)` — paper view changes /// Keyed by `(geometry_epoch, camera_generation)` — paper view changes
/// on zoom too, so culled wire output depends on camera. /// on zoom too, so culled wire output depends on camera.
paper_sheet_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>, paper_sheet_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
/// Per-viewport projected wire cache for the paper canvas (2-D Iced widget). /// Per-viewport projected wire cache for paper-space content viewports.
/// Stores projected + clipped wires in paper-space coordinates. /// Stores projected + clipped wires in paper-space coordinates.
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>). /// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>, paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
@ -732,7 +732,7 @@ impl Scene {
pub(super) fn view_world_aabb(&self) -> Option<[f32; 4]> { pub(super) fn view_world_aabb(&self) -> Option<[f32; 4]> {
if self.current_layout != "Model" { if self.current_layout != "Model" {
// Paper-space viewport composition handles its own culling; the // Paper-space viewport composition handles its own culling; the
// top-level paper canvas is small enough not to need it. // top-level paper view is small enough not to need it.
return None; return None;
} }
// Until the first explicit camera move (typically `fit_all()` after // Until the first explicit camera move (typically `fit_all()` after
@ -771,12 +771,10 @@ impl Scene {
/// World units per screen pixel at the current viewport size. Returns /// World units per screen pixel at the current viewport size. Returns
/// `None` until the first render captures real bounds. /// `None` until the first render captures real bounds.
/// ///
/// Also returns `None` in paper space: `set_render_pixel_scale` is only /// Also returns `None` in paper space: `last_world_per_pixel` tracks the
/// fed by the shader pipeline (`build_primitive`), and the paper layout /// model camera, so a cached value applied to mm-sheet entity AABBs would
/// renders through `PaperCanvas` (an Iced 2-D canvas) which never touches /// be a stale model-world wpp and cull every paper-space annotation.
/// it. Whatever value is cached would be a stale model-world wpp and, /// Matches the same skip already in `view_world_aabb`.
/// applied to mm-sheet entity AABBs, would cull every paper-space
/// annotation. Matches the same skip already in `view_world_aabb`.
pub(super) fn world_per_pixel(&self) -> Option<f32> { pub(super) fn world_per_pixel(&self) -> Option<f32> {
if self.current_layout != "Model" { if self.current_layout != "Model" {
return None; return None;
@ -1438,9 +1436,8 @@ impl Scene {
arc arc
} }
/// Cached tessellation of the current layout block's paper-space entities. /// Cached tessellation of the current layout block's paper-space entities,
/// Shared by both `entity_wires_arc()` and `paper_canvas_wires()` so a single /// shared by `entity_wires_arc()` and the GPU sheet viewport.
/// cache miss triggers only one tessellation pass, not two.
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> { fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
let key = (self.geometry_epoch, self.camera_generation); let key = (self.geometry_epoch, self.camera_generation);
{ {
@ -1452,7 +1449,15 @@ impl Scene {
} }
} }
let layout_block = self.current_layout_block_handle(); let layout_block = self.current_layout_block_handle();
let arc = Arc::new(self.wires_for_block(layout_block)); let mut wires = self.wires_for_block(layout_block);
// The overall "sheet" viewport now IS the paper view itself, so its own
// border rectangle must not be drawn as an entity on the sheet.
let sheet = self.current_layout_sheet_viewport_handle();
if sheet.is_valid() {
let sheet_name = sheet.value().to_string();
wires.retain(|w| w.name != sheet_name);
}
let arc = Arc::new(wires);
*self.paper_sheet_cache.borrow_mut() = Some((key, Arc::clone(&arc))); *self.paper_sheet_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
arc arc
} }
@ -5784,11 +5789,11 @@ impl Scene {
} }
/// Convert a paper-space Viewport entity's position/size into a pixel /// Convert a paper-space Viewport entity's position/size into a pixel
/// `Rectangle` relative to the top-left of the paper canvas. /// `Rectangle` relative to the top-left of the canvas.
/// ///
/// Uses the same camera-based ortho transform as `PaperCanvas::draw()` so /// Uses the same top-down ortho transform as the GPU sheet viewport so the
/// that the overlay lands exactly over the drawn viewport border regardless /// overlay lands exactly over the drawn viewport border regardless of zoom
/// of zoom or pan level. /// or pan level.
pub fn viewport_screen_rect( pub fn viewport_screen_rect(
&self, &self,
vp_handle: Handle, vp_handle: Handle,
@ -5812,7 +5817,7 @@ impl Scene {
let ty = cam.target.y; let ty = cam.target.y;
drop(cam); drop(cam);
// Mirror the to_px closure in PaperCanvas::draw(). // Top-down ortho mapping matching the GPU sheet viewport's camera.
let to_px = |wx: f32, wy: f32| -> (f32, f32) { let to_px = |wx: f32, wy: f32| -> (f32, f32) {
let x = (wx - tx + half_w) / (2.0 * half_w) * canvas_w; let x = (wx - tx + half_w) / (2.0 * half_w) * canvas_w;
let y = (ty + half_h - wy) / (2.0 * half_h) * canvas_h; let y = (ty + half_h - wy) / (2.0 * half_h) * canvas_h;
@ -5840,7 +5845,8 @@ impl Scene {
// ── Paper-space helpers ─────────────────────────────────────────────── // ── Paper-space helpers ───────────────────────────────────────────────
/// Hatch fills for the paper-space 2-D canvas. The GPU-rendered /// Paper-layout hatch fills, restricted to the active layout block (used by
/// paper-space hatch hit-testing / export). The GPU-rendered
/// content viewports already draw model-block hatches inside their /// content viewports already draw model-block hatches inside their
/// own scissor; including those here would also draw them on the /// own scissor; including those here would also draw them on the
/// paper sheet through the paper camera (huge / off-position), so /// paper sheet through the paper camera (huge / off-position), so
@ -5886,7 +5892,7 @@ impl Scene {
Arc::new(models) Arc::new(models)
} }
/// Wipeout fills for the paper-space 2-D canvas. Same rationale as /// Paper-layout wipeout fills (paper hit-testing / export). Same rationale as
/// `paper_canvas_hatches` — only include wipeouts owned by the /// `paper_canvas_hatches` — only include wipeouts owned by the
/// active paper layout block, so model wipeouts (drawn through their /// active paper layout block, so model wipeouts (drawn through their
/// content viewport's GPU pipeline) don't get a second mis-projected /// content viewport's GPU pipeline) don't get a second mis-projected
@ -5940,8 +5946,8 @@ impl Scene {
} }
/// Build a Camera oriented and scaled to match a paper-space Viewport entity. /// Build a Camera oriented and scaled to match a paper-space Viewport entity.
/// Used by `ViewportPane::Paper` to render model-space content through the /// Used by `active_viewports` to render model-space content through each
/// viewport's own view direction and scale. /// content viewport's own view direction and scale.
fn camera_for_viewport(&self, vp_handle: Handle) -> Option<camera::Camera> { fn camera_for_viewport(&self, vp_handle: Handle) -> Option<camera::Camera> {
let vp = match self.document.get_entity(vp_handle) { let vp = match self.document.get_entity(vp_handle) {
Some(EntityType::Viewport(vp)) => vp, Some(EntityType::Viewport(vp)) => vp,

View file

@ -535,18 +535,15 @@ impl Scene {
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0)); let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
let instances = self.active_viewports(canvas.0, canvas.1, model_render_mode); let instances = self.active_viewports(canvas.0, canvas.1, model_render_mode);
// Transparent clear — outside drawn geometry the resolve texture // Transparent clear — outside drawn geometry the resolve texture
// stays at alpha=0, so the alpha-blended blit reveals whatever the // stays at alpha=0, so the alpha-blended blit reveals the container
// underlying widget painted (model layout: container bg; paper // background (model bg, or the desk colour in a paper layout).
// layout: PaperCanvas sheet — including inside content viewport
// rectangles).
let bg_color = [0.0, 0.0, 0.0, 0.0]; let bg_color = [0.0, 0.0, 0.0, 0.0];
let viewports: Vec<ViewportData> = instances let viewports: Vec<ViewportData> = instances
.iter() .iter()
.filter_map(|inst| self.viewport_data_for(inst, canvas, hover_region)) .filter_map(|inst| self.viewport_data_for(inst, canvas, hover_region))
.collect(); .collect();
// Empty viewports → blit nothing. The widget container's // Empty viewports → blit nothing; the container background (model bg
// background (model bg or the PaperCanvas widget below) stays // or the paper desk colour) stays visible.
// visible. This happens in paper layouts without content viewports.
Primitive { viewports, bg_color } Primitive { viewports, bg_color }
} }