fix(paper): give the sheet back its desk and its edge

A paper layout painted its whole canvas in the page colour, so the sheet
had no visible edge — zooming out never reached one, because the surround
was the same colour as the page. Floating viewports then painted
themselves black over it.

Both come from the same idea, that a viewport's canvas is a colour it can
be handed. What lies behind a viewport depends on what the viewport is:

- The full-canvas sheet shows the desk. `paper_bg_color` is the page, and
  the page is a bounded rectangle `paper_sheet_fill` already draws; using
  it for the canvas too erased the boundary between them. The desk is now
  its own colour, defined once in the scene and read by both the widget
  and the renderer, so the two cannot drift apart.
- A floating viewport sits on that sheet and must paint no canvas at all.
  Handing it a transparent one does not work: the background shader
  returns an opaque colour whatever alpha it is given, so alpha zero came
  out black. Skipping the pass is the only way to ask for see-through, so
  `skip_background` now suppresses both the clear and the pass. It is part
  of the frame signature too — without that, a cached frame would keep
  showing the canvas the flag was just turned off for.

Shading and fog still blend toward what is behind a floating viewport,
which is the page, so the uniform keeps that colour even though nothing
paints it.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-08-08 00:11:47 +03:00
1 parent 6cbec232bb
commit 62dfbfe2be
4 files changed
+45 -9

No files matched your search

+6 -4
View File
@@ -34,11 +34,13 @@ use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W};
pub(in crate::app) use overlay::{MTEXT_TEXT_ID, TEXT_INLINE_ID};
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
/// The desk shown around the sheet, as the widget wants it. One definition,
/// shared with the renderer that clears the sheet viewport to the same thing.
const PAPER_SPACE_BACKGROUND: Color = Color {
r: 138.0 / 255.0,
g: 138.0 / 255.0,
b: 138.0 / 255.0,
a: 1.0,
r: crate::scene::PAPER_DESK_COLOR[0],
g: crate::scene::PAPER_DESK_COLOR[1],
b: crate::scene::PAPER_DESK_COLOR[2],
a: crate::scene::PAPER_DESK_COLOR[3],
};
/// Base surface directly under the crosshair. Paper content viewports render
+6
View File
@@ -285,6 +285,12 @@ struct GeometryDelta {
full: bool,
}
/// The desk a paper layout lays its sheet on — the surface visible around the
/// page. Distinct from both the page (`paper_bg_color`) and the model canvas
/// (`bg_color`); if the desk took either of those colours the page edge would
/// vanish into its own surround.
pub const PAPER_DESK_COLOR: [f32; 4] = [138.0 / 255.0, 138.0 / 255.0, 138.0 / 255.0, 1.0];
const CACHE_CATEGORY_ANNOTATIVE: u16 = 1 << 0;
const CACHE_CATEGORY_HATCH: u16 = 1 << 1;
const CACHE_CATEGORY_WIPEOUT: u16 = 1 << 2;
+14 -5
View File
@@ -340,6 +340,9 @@ pub struct Pipeline {
/// `render` skips the (per-pixel, GPU-dominating) hatch pass this frame. The
/// scene-render cache holds the full-quality frame once the view settles.
pub skip_hatch_frame: bool,
/// Skip the canvas entirely — no clear colour, no background pass — so
/// whatever was drawn underneath this viewport shows through it.
pub skip_background: bool,
/// Stable identity of the viewport that last used this (index-addressed)
/// pipeline slot. The renderer's viewport list drops off-canvas viewports,
/// so a slot can be reused by a *different* viewport across frames; when the
@@ -2015,6 +2018,7 @@ impl Pipeline {
render_sig: u64::MAX,
skip_geometry: false,
skip_hatch_frame: false,
skip_background: false,
slot_id: u64::MAX,
}
}
@@ -3282,7 +3286,7 @@ impl Pipeline {
};
let msaa = &self.msaa_view;
let [r, g, b, a] = bg_color;
let clear_color = if self.clip_boundary.is_some() {
let clear_color = if self.clip_boundary.is_some() || self.skip_background {
wgpu::Color::TRANSPARENT
} else {
wgpu::Color {
@@ -3393,10 +3397,15 @@ impl Pipeline {
pass.set_vertex_buffer(0, vbuf.slice(..));
pass.draw(0..*vcount, 0..1);
}
pass.set_pipeline(&self.background_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
pass.draw(0..3, 0..1);
// The background shader returns an opaque colour whatever alpha it
// is handed, so a see-through viewport cannot be asked for as a
// transparent background — the pass has to not run.
if !self.skip_background {
pass.set_pipeline(&self.background_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
pass.draw(0..3, 0..1);
}
// The capability-selected façade dispatches storage or texture
// draws before wires so outlines remain on top in either backend.
// Skipped while navigating because per-pixel hatch work dominates
+19
View File
@@ -202,6 +202,10 @@ pub struct ViewportData {
/// into the render signature so the settle frame re-renders hatches once and
/// the scene-render cache holds it. See [`Scene::navigating_lod`].
pub(in crate::scene) skip_hatch: bool,
/// True when this viewport must not paint a canvas of its own. A floating
/// viewport on a paper layout sits ON the sheet: anything it painted first
/// would cover the page it is supposed to be a window in.
pub(in crate::scene) skip_background: bool,
pub(in crate::scene) geometry_epoch: u64,
/// Camera generation captured when this Primitive was assembled. Paired
/// with `geometry_epoch` so the per-frame scissor / LOD recompute runs.
@@ -434,6 +438,7 @@ impl shader::Primitive for Primitive {
inner.skip_geometry = skip;
// Interaction LOD: skip the hatch draw this frame while navigating.
inner.skip_hatch_frame = vp.skip_hatch;
inner.skip_background = vp.skip_background;
if skip {
if vp.show_viewcube {
inner.viewcube.upload(
@@ -1250,6 +1255,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
// Interaction-LOD hatch suppression: differs the signature so the settle
// frame (skip_hatch flips false) re-renders with hatches and re-caches.
vp.skip_hatch.hash(&mut h);
vp.skip_background.hash(&mut h);
clip_w.hash(&mut h);
clip_h.hash(&mut h);
// Live overlay (command preview / interim / grip drag). Small — a handful
@@ -1912,7 +1918,19 @@ impl Scene {
&self,
viewport: &ViewportInstance,
) -> ViewportDisplaySettings {
// What lies behind a viewport depends on what the viewport is. The
// full-canvas paper sheet shows the desk, and the page is drawn on top
// of it by `paper_sheet_fill` — give the desk the page's own colour and
// the edge disappears into a same-coloured surround that no amount of
// zooming out reveals. A floating viewport sits ON that sheet, so it
// paints no canvas at all; anything else would erase the page beneath
// it. Only model space clears to the editor background.
let canvas_background = if viewport.paper_sheet {
crate::scene::PAPER_DESK_COLOR
} else if self.current_layout != "Model" {
// This viewport paints no canvas of its own (`skip_background`),
// but shading and fog still blend toward whatever is behind it —
// and behind a floating viewport is the page.
self.paper_bg_color
} else {
self.bg_color
@@ -3594,6 +3612,7 @@ impl Scene {
// (hatched) frame once it settles. Only applied to the on-screen
// Model / paper content — the paper *sheet* keeps its fills.
skip_hatch: self.hatch_lod_enabled() && !inst.paper_sheet && self.navigating_lod(),
skip_background: !inst.paper_sheet && self.current_layout != "Model",
geometry_epoch: self.geometry_epoch,
camera_generation: self.camera_generation,
wire_content_id,