From 459b525ffeb2bedfa67943109f11436c701dfee6 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Apr 2026 09:48:53 +0300 Subject: [PATCH] feat: replace paper-space shader with 2D Iced canvas widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paper space is now rendered as a 2D vector canvas (iced::widget::canvas) instead of a 3D shader widget. This allows direct interaction with paper-space entities (title blocks, viewport borders, annotations) without needing to enter MSPACE first. Changes: - src/scene/paper_canvas.rs: new PaperCanvas<'a> canvas::Program that renders paper wires (with linetype dashes), solid/gradient hatch fills, and wipeout backgrounds using the paper camera's orthographic transform. - Scene: three new public helpers (paper_canvas_wires, paper_canvas_hatches, paper_canvas_wipeouts) feed the canvas with the same data the old shader used, including inactive viewport projections and interim/preview wires. - view.rs: paper_canvas_view() now wraps PaperCanvas instead of shader(ViewportPane::paper_sheet(…)). The PaperViewportPane 3D shader overlay for the active MSPACE viewport is unchanged. Co-Authored-By: Claude Sonnet 4.6 --- src/app/view.rs | 8 +- src/scene/mod.rs | 25 +++++ src/scene/paper_canvas.rs | 193 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/scene/paper_canvas.rs diff --git a/src/app/view.rs b/src/app/view.rs index 4ea0c513c..0bf9480a4 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -4,9 +4,10 @@ use super::history::history_dropdown_labels; use super::helpers::grid_plane_from_camera; use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; use crate::scene::grip::grips_to_screen; +use crate::scene::paper_canvas::PaperCanvas; use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane}; use crate::ui::overlay; -use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row, Space}; +use iced::widget::{button, canvas, column, container, mouse_area, row, shader, stack, text, Row, Space}; use iced::window; use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme}; @@ -470,7 +471,10 @@ impl H7CAD { fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, Message> { let scene = &tab.scene; - let paper_sheet = shader(ViewportPane::paper_sheet(scene)) + // 2-D canvas for the paper sheet — paper entities, viewport borders, and + // inactive viewport projections are rendered as vector paths. This lets + // users select/edit paper-space entities directly without entering MSPACE. + let paper_sheet = canvas(PaperCanvas::new(scene)) .width(Fill) .height(Fill); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 768986c3c..4226cee0d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -10,6 +10,7 @@ pub mod hit_test; pub mod image_model; pub mod mesh_model; pub mod object; +pub mod paper_canvas; pub mod pipeline; pub mod properties; mod render; @@ -2214,6 +2215,30 @@ impl Scene { /// 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. + pub fn paper_canvas_wires(&self) -> Vec { + let layout_block = self.current_layout_block_handle(); + let mut wires = self.paper_sheet_wires(); + wires.extend(self.viewport_content_wires(layout_block, None, self.active_viewport)); + if let Some(iw) = &self.interim_wire { + wires.push(iw.clone()); + } + wires.extend(self.preview_wires.iter().cloned()); + wires + } + + /// Hatch fills for the paper-space canvas. + pub fn paper_canvas_hatches(&self) -> Vec { + self.synced_hatch_models() + } + + /// Wipeout (opaque background fill) models for the paper-space canvas. + pub fn paper_canvas_wipeouts(&self) -> Vec { + self.wipeout_models() + } + pub(super) fn paper_sheet_wires(&self) -> Vec { let layout_block = self.current_layout_block_handle(); let mut wires = self.wires_for_block(layout_block); diff --git a/src/scene/paper_canvas.rs b/src/scene/paper_canvas.rs new file mode 100644 index 000000000..7b0ab9c9e --- /dev/null +++ b/src/scene/paper_canvas.rs @@ -0,0 +1,193 @@ +//! 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. +//! +//! When MSPACE is active, a separate `PaperViewportPane` shader widget is +//! stacked on top of this canvas for the active viewport's 3-D content +//! (see `view.rs::paper_canvas_view()`). + +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()); + + // ── Background ──────────────────────────────────────────────────────── + let [r, g, b, a] = self.scene.paper_bg_color; + frame.fill_rectangle(Point::ORIGIN, bounds.size(), Color { r, g, b, a }); + + // ── Wipeout fills (rendered before wires, cover background) ────────── + for hatch in &self.scene.paper_canvas_wipeouts() { + draw_hatch(&mut frame, hatch, &to_px); + } + + // ── Hatch fills ─────────────────────────────────────────────────────── + for hatch in &self.scene.paper_canvas_hatches() { + draw_hatch(&mut frame, hatch, &to_px); + } + + // px-per-world-unit scale for linetype dash lengths. + let world_to_px_scale = if half_w > 0.0 { + bounds.width / (2.0 * half_w) + } else { + 1.0 + }; + + // ── Wires (entity lines + inactive viewport projections) ────────────── + for wire in &self.scene.paper_canvas_wires() { + let [r, g, b, a] = wire.color; + let color = Color { r, g, b, a }; + + let path = canvas::Path::new(|b| { + let mut started = false; + for &[wx, wy, _] in &wire.points { + if wx.is_nan() || wy.is_nan() { + started = false; + continue; + } + let p = to_px(wx, wy); + if started { + b.line_to(p); + } else { + b.move_to(p); + started = true; + } + } + }); + + // Convert WireModel linetype pattern (world units) to pixel lengths. + // Keep the Vec alive for the duration of frame.stroke(). + let dash_segments: Vec = if wire.pattern_length > 0.0 { + wire.pattern + .iter() + .take_while(|&&v| v != 0.0) + .map(|&v| v.abs() * world_to_px_scale) + .collect() + } else { + vec![] + }; + + frame.stroke( + &path, + canvas::Stroke { + style: canvas::Style::Solid(color), + width: wire.line_weight_px.max(1.0), + line_cap: canvas::LineCap::Square, + line_join: canvas::LineJoin::Miter, + line_dash: canvas::LineDash { + segments: &dash_segments, + offset: 0, + }, + }, + ); + } + + 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 }; + + let path = canvas::Path::new(|builder| { + let first = to_px(hatch.boundary[0][0], hatch.boundary[0][1]); + builder.move_to(first); + for &[x, y] in &hatch.boundary[1..] { + builder.line_to(to_px(x, y)); + } + builder.close(); + }); + + match &hatch.pattern { + HatchPattern::Solid => { + frame.fill(&path, color); + } + HatchPattern::Pattern(_) => { + // Pattern hatches: draw just the boundary outline for now. + frame.stroke( + &path, + 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); + } + } +} +