feat(paper): render the sheet as a top-locked GPU viewport

Paper space now renders its sheet through the unified shader, the same
path as model space: a full-canvas "sheet" ViewportInstance draws the
layout's own entities + viewport borders + interim/preview wires, with a
camera locked to the top/plan orientation (paper is 2-D and never
orbits). Floating content viewports overlay it.

- PaperCanvas no longer draws entity wires (still paints the desk, sheet
  and hatch/wipeout/image fills; full removal is the next step).
- Right-drag on the paper sheet no longer orbits the camera (it would
  corrupt the frame and skew subsequent pans).
- Fix floating-viewport orbit oscillation: orbit_active_viewport wrote
  view_direction.y negated while camera_for_viewport reads it un-negated,
  so each drag step read back a Y-mirrored camera and flipped the model
  between a rotation and its opposite every frame. Store eye directly,
  matching snap_active_viewport_to_direction and the reader.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-02 20:05:51 +03:00
1 parent 568422679d
commit 0ef99ff2fc
4 files changed
+65 -64

No files matched your search

+38 -4
View File
@@ -436,6 +436,10 @@ pub struct ViewportInstance {
pub render_mode: acadrust::entities::ViewportRenderMode,
/// `true` when this is the viewport receiving cursor input.
pub active: bool,
/// `true` for the full-canvas paper "sheet" viewport — the layout's own
/// view (paper-space entities, top-locked), the paper equivalent of the
/// Model view. Floating content viewports overlay it.
pub paper_sheet: bool,
}
/// One pane of the Model-space tiled viewport layout: the normalized screen
@@ -2691,16 +2695,18 @@ impl Scene {
};
cam.orbit(delta_x, delta_y);
// yaw_pitch_to_quat(y,p)*Z = (cos(p)*sin(y), -cos(p)*cos(y), sin(p))
// but view_direction convention (matching snap_active_viewport_to_angles) is
// (cos(p)*sin(y), +cos(p)*cos(y), sin(p)) ← Y has opposite sign.
// Negate Y when writing back so camera_for_viewport round-trips correctly.
// `camera_for_viewport` reconstructs the rotation so that
// `rotation * Z == view_direction` exactly (its `yaw = atan2(x, -y)`
// cancels the sign). Store `eye` directly so the orbit round-trips —
// negating Y here made each drag step read back a Y-mirrored camera,
// flipping the model between a rotation and its opposite every frame.
let eye = cam.rotation * glam::Vec3::Z;
if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
if vp.status.locked {
return;
}
vp.view_direction.x = eye.x as f64;
vp.view_direction.y = -eye.y as f64;
vp.view_direction.y = eye.y as f64;
vp.view_direction.z = eye.z as f64;
}
}
@@ -5699,12 +5705,37 @@ impl Scene {
camera,
render_mode: model_mode,
active: i == active,
paper_sheet: false,
}
})
.collect();
}
let layout_block = self.current_layout_block_handle();
let mut out: Vec<ViewportInstance> = Vec::new();
// The full-canvas sheet viewport renders the paper-space entities
// themselves — the layout's own view, drawn first so the floating
// content viewports overlay it. Its camera keeps the paper pan/zoom
// (target + ortho size) but is LOCKED to the top/plan orientation:
// paper is 2-D, so the sheet never orbits.
let mut sheet_cam = self.camera.borrow().clone();
sheet_cam.yaw = 0.0;
sheet_cam.pitch = std::f32::consts::FRAC_PI_2;
sheet_cam.rotation = camera::yaw_pitch_to_quat(0.0, std::f32::consts::FRAC_PI_2, 0.0);
sheet_cam.projection = camera::Projection::Orthographic;
out.push(ViewportInstance {
handle: Handle::NULL,
tile_idx: None,
screen_rect: iced::Rectangle {
x: 0.0,
y: 0.0,
width: canvas_w,
height: canvas_h,
},
camera: sheet_cam,
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
active: false,
paper_sheet: true,
});
for e in self.document.entities() {
let EntityType::Viewport(vp) = e else {
continue;
@@ -5728,6 +5759,7 @@ impl Scene {
camera,
render_mode: vp.render_mode,
active: self.active_viewport == Some(h),
paper_sheet: false,
});
}
out
@@ -5796,6 +5828,7 @@ impl Scene {
/// All wires needed to render the paper-space canvas (2D widget path).
/// Includes paper entities, paper boundary, inactive viewport projections
/// (excluding the active MSPACE viewport), plus interim/preview wires.
#[allow(dead_code)] // superseded by the GPU sheet viewport; removed in Stage 2.
pub fn paper_canvas_wires(&self) -> Arc<Vec<WireModel>> {
{
let cache = self.paper_canvas_cache.borrow();
@@ -5919,6 +5952,7 @@ impl Scene {
Arc::new(models)
}
#[allow(dead_code)] // superseded by the GPU sheet viewport; removed in Stage 2.
pub(super) fn paper_sheet_wires(&self) -> Vec<WireModel> {
(*self.paper_sheet_wires_arc()).clone()
}