diff --git a/src/app/view.rs b/src/app/view.rs index 7100e83ff..8dd4acdc5 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -4,12 +4,11 @@ use super::history::history_dropdown_labels; use super::document::{DynComponent, DynFieldEntry}; use super::{Message, OpenCADStudio}; use crate::scene::grip::{grips_to_screen, grips_to_screen_paper}; -use crate::scene::paper_canvas::PaperCanvas; use crate::scene::viewport_pane::ViewportPane; use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; use crate::ui::overlay; use iced::widget::{ - button, canvas, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, + button, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, Row, Space, }; use iced::window; @@ -470,15 +469,19 @@ impl OpenCADStudio { .width(Fill) .height(Fill) } else if is_paper { - // Paper layout: 2-D sheet + paper entities + viewport borders - // underneath, unified GPU shader on top (only paints inside - // each content viewport's scissor rect so the sheet shows - // through outside them). - let paper_sheet: Element<'_, Message> = - canvas(PaperCanvas::new(&tab.scene)).width(Fill).height(Fill).into(); + // Paper layout: the GPU shader renders everything — the desk is the + // container background, the white sheet + paper entities + borders + // come from the full-canvas top-locked "sheet" viewport, and the + // floating content viewports overlay it (same path as model space). + const DESK: Color = Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 }; stack![ - paper_sheet, - container(viewport_3d).width(Fill).height(Fill), + container(viewport_3d) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(DESK)), + ..Default::default() + }) + .width(Fill) + .height(Fill), selection_overlay, viewport_mouse, ] diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1372c3252..3cd962e8d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -12,7 +12,6 @@ pub mod image_model; pub mod mesh_model; pub mod model_solid; pub mod object; -pub mod paper_canvas; pub mod pipeline; pub mod properties; pub mod quadtree; @@ -609,10 +608,6 @@ pub struct Scene { /// Stores projected + clipped wires in paper-space coordinates. /// Maps vp_handle → (geometry_epoch, Vec). paper_projected_cache: RefCell)>>, - /// Full paper-canvas wire list (sheet + inactive-viewport projections + interim/preview). - /// Keyed by geometry_epoch — valid for all navigation frames (pan/zoom do not bump epoch). - /// Returns Arc so paper_canvas_wires() is O(1) on cache hits. - paper_canvas_cache: RefCell>)>>, /// Active layout name — "Model" or a paper space layout name. pub current_layout: String, /// GPU render data for hatch fills, keyed by the DXF entity Handle. @@ -709,7 +704,6 @@ impl Scene { viewport_wire_cache: RefCell::new(HashMap::new()), paper_sheet_cache: RefCell::new(None), paper_projected_cache: RefCell::new(HashMap::new()), - paper_canvas_cache: RefCell::new(None), current_layout: "Model".to_string(), hatches: HashMap::new(), meshes: HashMap::new(), @@ -1137,6 +1131,30 @@ impl Scene { /// Returns `(min, max)` paper-space limits for the current layout, or `None` /// when in Model space. Falls back to A4 landscape if nothing reliable is found. + /// A solid white fill covering the paper sheet's printable area, rendered + /// by the GPU hatch pipeline behind the paper entities. Replaces the 2-D + /// white-rectangle the old PaperCanvas drew. `None` in model space or when + /// the layout has no limits. + pub(super) fn paper_sheet_fill(&self) -> Option { + let ((x0, y0), (x1, y1)) = self.paper_limits()?; + let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); + Some(HatchModel { + world_origin: [0.0, 0.0], + boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), + pattern: crate::scene::hatch_model::HatchPattern::Solid, + name: "SOLID".to_string(), + color: self.paper_bg_color, + angle_offset: 0.0, + scale: 1.0, + vp_scissor: None, + // Draw-order bias is signed: entity fills/wires land in (-1, 1) + // (0 = neutral). A value below -1 forces the sheet strictly behind + // EVERY object, in every case, with a tiny z offset (BIAS = 0.001, + // so no far-plane clipping). The sheet is the canvas, never on top. + draw_depth: -2.0, + }) + } + pub fn paper_limits(&self) -> Option<((f64, f64), (f64, f64))> { if self.current_layout == "Model" { return None; @@ -5820,38 +5838,7 @@ impl Scene { }) } - // ── ViewportPane helpers ────────────────────────────────────────────── - - /// Paper-space entity wires only (title blocks, frames, borders). - /// Does NOT include viewport content projection — that is handled by - /// individual ViewportPane::Paper widgets layered on top. - /// 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> { - { - let cache = self.paper_canvas_cache.borrow(); - if let Some((cached_epoch, ref arc)) = *cache { - if cached_epoch == self.geometry_epoch { - return Arc::clone(arc); - } - } - } - // The unified GPU shader draws every content viewport (active and - // inactive) directly through its own camera + scissor, so the - // paper canvas no longer needs to CPU-project model content onto - // the sheet. `paper_sheet_wires()` keeps the title-block / - // annotation / viewport-border 2-D pass that GPU does not handle. - let mut wires = self.paper_sheet_wires(); - if let Some(iw) = &self.interim_wire { - wires.push(iw.clone()); - } - wires.extend(self.preview_wires.iter().cloned()); - let arc = Arc::new(wires); - *self.paper_canvas_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); - arc - } + // ── Paper-space helpers ─────────────────────────────────────────────── /// Hatch fills for the paper-space 2-D canvas. The GPU-rendered /// content viewports already draw model-block hatches inside their @@ -5952,11 +5939,6 @@ 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 { - (*self.paper_sheet_wires_arc()).clone() - } - /// Build a Camera oriented and scaled to match a paper-space Viewport entity. /// Used by `ViewportPane::Paper` to render model-space content through the /// viewport's own view direction and scale. diff --git a/src/scene/paper_canvas.rs b/src/scene/paper_canvas.rs deleted file mode 100644 index f2972e43b..000000000 --- a/src/scene/paper_canvas.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! 2-D Iced canvas widget for the paper-space view. -//! -//! Replaces the 3-D shader widget used for the paper sheet so that paper-space -//! entities (title blocks, annotations, viewport borders) can be interacted -//! with directly — no need to enter MSPACE just to click on them. -//! -//! A single unified `ViewportPane` shader widget is stacked on top of this -//! canvas to render every content viewport's 3-D model content into its own -//! scissor rectangle — outside those rectangles the shader does not paint, -//! so the paper sheet drawn here shows through. - -use iced::widget::canvas; -use iced::{mouse, Color, Point, Rectangle}; - -use super::hatch_model::{HatchModel, HatchPattern}; -use super::Scene; -use crate::app::Message; - -// ── PaperCanvas ─────────────────────────────────────────────────────────────── - -pub struct PaperCanvas<'a> { - pub scene: &'a Scene, -} - -impl<'a> PaperCanvas<'a> { - pub fn new(scene: &'a Scene) -> Self { - Self { scene } - } -} - -// ── canvas::Program impl ────────────────────────────────────────────────────── - -impl<'a> canvas::Program for PaperCanvas<'a> { - type State = (); - - fn draw( - &self, - _state: &(), - renderer: &iced::Renderer, - _theme: &iced::Theme, - bounds: Rectangle, - _cursor: mouse::Cursor, - ) -> Vec { - // Update vp_size so viewport_screen_rect() knows the canvas dimensions - // and can position the MSPACE shader overlay correctly. - self.scene.selection.borrow_mut().vp_size = (bounds.width, bounds.height); - - let cam = self.scene.camera.borrow(); - let aspect = if bounds.height > 0.0 { - bounds.width / bounds.height - } else { - 1.0 - }; - let half_h = cam.ortho_size(); - let half_w = half_h * aspect; - let tx = cam.target.x; - let ty = cam.target.y; - drop(cam); - - // Closure: paper-space world coords → canvas pixel coords. - let to_px = move |wx: f32, wy: f32| Point { - x: (wx - tx + half_w) / (2.0 * half_w) * bounds.width, - y: (ty + half_h - wy) / (2.0 * half_h) * bounds.height, - }; - - let mut frame = canvas::Frame::new(renderer, bounds.size()); - - // ── Desk background (area outside the paper sheet) ──────────────────── - const DESK: Color = Color { - r: 0.22, - g: 0.24, - b: 0.28, - a: 1.0, - }; - frame.fill_rectangle(Point::ORIGIN, bounds.size(), DESK); - - // ── White paper area — use layout paper limits (actual paper size). - if let Some(((px0, py0), (px1, py1))) = self - .scene - .paper_limits() - .map(|((x0, y0), (x1, y1))| ((x0 as f32, y0 as f32), (x1 as f32, y1 as f32))) - { - let tl = to_px(px0, py1); - let br = to_px(px1, py0); - let pw = br.x - tl.x; - let ph = br.y - tl.y; - if pw > 0.0 && ph > 0.0 { - let [r, g, b, a] = self.scene.paper_bg_color; - frame.fill_rectangle(tl, iced::Size::new(pw, ph), Color { r, g, b, a }); - } - } - - // ── Wipeout fills (rendered before wires, cover background) ────────── - for hatch in self.scene.paper_canvas_wipeouts().iter() { - draw_hatch(&mut frame, hatch, &to_px); - } - - // ── Hatch fills ─────────────────────────────────────────────────────── - for hatch in self.scene.paper_canvas_hatches().iter() { - draw_hatch(&mut frame, hatch, &to_px); - } - - // Entity lines, viewport borders and interim/preview wires are now - // rendered by the GPU shader's full-canvas paper "sheet" viewport, so - // the 2-D canvas only paints the desk + sheet + fills beneath it. - vec![frame.into_geometry()] - } -} - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -/// Draw one HatchModel (solid, gradient, or pattern) onto `frame`. -/// Patterns and gradients are approximated as solid fills in 2-D canvas mode. -fn draw_hatch(frame: &mut canvas::Frame, hatch: &HatchModel, to_px: &impl Fn(f32, f32) -> Point) { - if hatch.boundary.is_empty() { - return; - } - - let [r, g, b, a] = hatch.color; - let color = Color { r, g, b, a }; - - // Reconstruct offset-rel WCS from stored small offsets + f64 anchor. - let ox = hatch.world_origin[0] as f32; - let oy = hatch.world_origin[1] as f32; - // `boundary` may carry multiple disconnected sub-paths separated by - // NaN-NaN sentinels (multi-region hatches with islands / holes). - // Forwarding NaN to lyon's path builder panics - // (`IncorrectActiveEdgeOrder`) — start a fresh sub-path on each - // sentinel so each region is closed and filled independently. - let path = canvas::Path::new(|builder| { - let mut active = false; - for &[x, y] in hatch.boundary.iter() { - if x.is_nan() || y.is_nan() { - if active { - builder.close(); - active = false; - } - continue; - } - let p = to_px(x + ox, y + oy); - if !active { - builder.move_to(p); - active = true; - } else { - builder.line_to(p); - } - } - if active { - builder.close(); - } - }); - - match &hatch.pattern { - HatchPattern::Solid => { - frame.fill(&path, color); - } - HatchPattern::Pattern(_) => { - // Rasterise the PAT line families clipped to the boundary so - // the paper canvas shows the actual hatch lines (ANSI31, etc.) - // instead of just the outline. Matches the GPU shader output - // for model-space hatches. - for [a, b] in hatch.pattern_segments() { - let seg = canvas::Path::new(|builder| { - builder.move_to(to_px(a[0], a[1])); - builder.line_to(to_px(b[0], b[1])); - }); - frame.stroke( - &seg, - canvas::Stroke { - style: canvas::Style::Solid(color), - width: 1.0, - ..Default::default() - }, - ); - } - } - HatchPattern::Gradient { color2, .. } => { - // Gradient: average the two colours as a solid fill. - let avg = Color { - r: (color.r + color2[0]) * 0.5, - g: (color.g + color2[1]) * 0.5, - b: (color.b + color2[2]) * 0.5, - a: (color.a + color2[3]) * 0.5, - }; - frame.fill(&path, avg); - } - } -} diff --git a/src/scene/render.rs b/src/scene/render.rs index f13d16ef1..401d78e2a 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -653,14 +653,22 @@ impl Scene { height: visible_h / canvas.1, }; - // Stage 1: the paper sheet instance contributes only entity wires; its - // hatch / wipeout / image fills are still drawn by PaperCanvas beneath - // the shader. (Stage 2 moves those into the shader and drops PaperCanvas.) - let (hatches, wipeout_hatches, images) = if inst.paper_sheet { - (Arc::new(Vec::new()), Arc::new(Vec::new()), Arc::new(Vec::new())) + // 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 { + 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) } else { - (self.hatch_models_arc(), self.wipeout_models_arc(), self.images_arc()) + self.hatch_models_arc() }; + let wipeout_hatches = self.wipeout_models_arc(); + let images = self.images_arc(); Some(ViewportData { wires: all_wires,