diff --git a/src/app/view.rs b/src/app/view.rs index 58712e8f..4c63d5f0 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -4,7 +4,7 @@ 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::viewport_pane::ViewportPane; +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::window; @@ -457,22 +457,52 @@ impl H7CAD { // ── Paper canvas ────────────────────────────────────────────────────────── // -// Renders the full paper canvas as a single full-size shader widget using the -// PaperSheet mode, which includes both paper-space entities (title blocks, -// frames, borders) and model-space content projected through each viewport's -// view matrix into paper-space coordinates. +// PSPACE: single full-canvas PaperSheet widget — renders paper entities plus +// model content of all viewports via CPU projection. // -// Note: A per-viewport widget approach (ViewportPane::Paper) was attempted but -// does not work correctly because Iced 0.14 batches all shader prepare() calls -// before any render() calls, causing widgets that share the same Pipeline type -// to overwrite each other's GPU buffers. The CPU-projection approach used here -// is the correct solution within Iced's shader framework. +// MSPACE (active viewport): PaperSheet widget (excludes the active viewport +// from its CPU projection) + a PaperViewportPane widget overlaid at the +// active viewport's screen-space position. PaperViewportPane uses a +// distinct pipeline type (PaperViewportPipeline) so Iced's per-type storage +// keeps the two prepare() calls from overwriting each other. fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, Message> { - shader(ViewportPane::paper_sheet(&tab.scene)) + let scene = &tab.scene; + + let paper_sheet = shader(ViewportPane::paper_sheet(scene)) .width(Fill) - .height(Fill) - .into() + .height(Fill); + + if let Some(vp_handle) = scene.active_viewport { + let (canvas_w, canvas_h) = scene.selection.borrow().vp_size; + if let Some(rect) = scene.viewport_screen_rect(vp_handle, (canvas_w, canvas_h)) { + let w = rect.width.max(1.0); + let h = rect.height.max(1.0); + let x = rect.x.max(0.0); + let y = rect.y.max(0.0); + + let vp_widget = shader(PaperViewportPane::new(scene, vp_handle)) + .width(iced::Length::Fixed(w)) + .height(iced::Length::Fixed(h)); + + let positioned = column![ + Space::new().height(iced::Length::Fixed(y)), + row![ + Space::new().width(iced::Length::Fixed(x)), + vp_widget, + ], + ] + .width(Fill) + .height(Fill); + + return stack![paper_sheet, positioned] + .width(Fill) + .height(Fill) + .into(); + } + } + + paper_sheet.into() } // ── Document tab bar ─────────────────────────────────────────────────────── diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 39ffef8d..b48dd102 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -351,7 +351,7 @@ impl Scene { if let Some(((x0, y0), (x1, y1))) = self.paper_limits() { wires.insert(0, paper_boundary_wire(x0 as f32, y0 as f32, x1 as f32, y1 as f32)); } - wires.extend(self.viewport_content_wires(layout_block, None)); + wires.extend(self.viewport_content_wires(layout_block, None, None)); } wires } @@ -375,7 +375,7 @@ impl Scene { } Some(vp_handle) => { // MSPACE: only model content visible through the active viewport - self.viewport_content_wires(layout_block, Some(vp_handle)) + self.viewport_content_wires(layout_block, Some(vp_handle), None) } } } @@ -667,7 +667,12 @@ impl Scene { /// Collect model-space wires projected into paper space for all (or one specific) /// user viewports. `only_vp = Some(h)` restricts output to that viewport. - fn viewport_content_wires(&self, paper_block: Handle, only_vp: Option) -> Vec { + fn viewport_content_wires( + &self, + paper_block: Handle, + only_vp: Option, + exclude_vp: Option, + ) -> Vec { use acadrust::entities::Viewport; use std::collections::HashSet as HSet; @@ -682,6 +687,7 @@ impl Scene { && vp.common.owner_handle == paper_block && vp.status.is_on && only_vp.map_or(true, |h| vp.common.handle == h) + && exclude_vp.map_or(true, |h| vp.common.handle != h) }) .collect(); @@ -2104,8 +2110,7 @@ impl Scene { /// /// `canvas_px` — the pixel dimensions of the area that shows the paper. /// Map a paper-space viewport into screen-pixel coordinates. - /// Needed when per-viewport shader widgets are revived (see `ViewportPaneMode::Paper`). - #[allow(dead_code)] + /// Used to position the active-viewport widget in MSPACE paper space. pub fn viewport_screen_rect( &self, vp_handle: Handle, diff --git a/src/scene/render.rs b/src/scene/render.rs index bc14f3b2..62544ccf 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -14,6 +14,56 @@ use super::pipeline::Pipeline; use super::tessellate; use super::{HatchModel, ImageModel, MeshModel, Scene, Uniforms, WireModel}; +// ── PaperViewportPipeline / PaperViewportPrimitive ──────────────────────── +// +// Newtype wrappers around Pipeline / Primitive so that the active-MSPACE +// viewport widget gets its own Iced storage entry (keyed by TypeId of the +// Pipeline type). This prevents the shared-pipeline prepare() overwrite +// that occurs when PaperSheet and the viewport widget both use `Pipeline`. + +/// Dedicated pipeline for the MSPACE active-viewport shader widget. +pub struct PaperViewportPipeline(pub(super) Pipeline); + +impl iced::widget::shader::Pipeline for PaperViewportPipeline { + fn new( + device: &iced::wgpu::Device, + queue: &iced::wgpu::Queue, + format: iced::wgpu::TextureFormat, + ) -> Self { + Self(Pipeline::new(device, queue, format)) + } +} + +/// Primitive returned by `PaperViewportPane`; delegates everything to the +/// inner `Primitive` via the dedicated `PaperViewportPipeline`. +#[derive(Debug)] +pub struct PaperViewportPrimitive(pub(super) Primitive); + +impl shader::Primitive for PaperViewportPrimitive { + type Pipeline = PaperViewportPipeline; + + fn prepare( + &self, + pipeline: &mut PaperViewportPipeline, + device: &iced::wgpu::Device, + queue: &iced::wgpu::Queue, + bounds: &Rectangle, + viewport: &Viewport, + ) { + self.0.prepare(&mut pipeline.0, device, queue, bounds, viewport); + } + + fn render( + &self, + pipeline: &PaperViewportPipeline, + encoder: &mut iced::wgpu::CommandEncoder, + target: &iced::wgpu::TextureView, + clip: &Rectangle, + ) { + self.0.render(&pipeline.0, encoder, target, clip); + } +} + // ── Camera hover state (shader::Program::State) ─────────────────────────── #[derive(Clone, Default)] @@ -209,7 +259,9 @@ impl Scene { let layout_block = self.current_layout_block_handle(); let mut wires = self.paper_sheet_wires(); - wires.extend(self.viewport_content_wires(layout_block, None)); + // When MSPACE is active, exclude that viewport from the CPU projection — + // it is rendered in 3D by the separate PaperViewportPane widget. + wires.extend(self.viewport_content_wires(layout_block, None, self.active_viewport)); if let Some(iw) = &self.interim_wire { wires.push(iw.clone()); } @@ -260,6 +312,17 @@ impl Scene { } } + /// Wrap `build_viewport_primitive()` in `PaperViewportPrimitive` for use + /// by `PaperViewportPane`, which needs its own dedicated pipeline type. + pub(super) fn build_active_viewport_primitive( + &self, + vp_handle: Handle, + hover_region: Option, + bounds: Rectangle, + ) -> PaperViewportPrimitive { + PaperViewportPrimitive(self.build_viewport_primitive(vp_handle, hover_region, bounds)) + } + /// Update viewcube hover state from cursor position within `bounds`. pub(super) fn update_viewcube_state( &self, diff --git a/src/scene/viewport_pane.rs b/src/scene/viewport_pane.rs index 20a8cc05..add192fe 100644 --- a/src/scene/viewport_pane.rs +++ b/src/scene/viewport_pane.rs @@ -1,4 +1,4 @@ -use super::render::{CameraState, Primitive}; +use super::render::{CameraState, PaperViewportPrimitive, Primitive}; use super::Scene; use acadrust::Handle; use iced::widget::shader; @@ -48,7 +48,59 @@ impl<'a> ViewportPane<'a> { } } -// ── shader::Program impl ────────────────────────────────────────────────── +// ── PaperViewportPane ───────────────────────────────────────────────────── +// +// A shader widget for the MSPACE active viewport. Uses PaperViewportPrimitive +// (and therefore PaperViewportPipeline) so it gets its own Iced storage entry, +// separate from the ViewportPane/PaperSheet pipeline. + +pub struct PaperViewportPane<'a> { + pub scene: &'a Scene, + pub handle: Handle, +} + +impl<'a> PaperViewportPane<'a> { + pub fn new(scene: &'a Scene, handle: Handle) -> Self { + Self { scene, handle } + } +} + +impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for PaperViewportPane<'a> { + type State = CameraState; + type Primitive = PaperViewportPrimitive; + + fn draw( + &self, + state: &Self::State, + _cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + self.scene.build_active_viewport_primitive(self.handle, state.hover_region, bounds) + } + + fn update( + &self, + state: &mut Self::State, + event: &Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + self.scene.update_viewcube_state(state, bounds, cursor); + let _ = event; + None + } + + fn mouse_interaction( + &self, + state: &Self::State, + _b: Rectangle, + _c: mouse::Cursor, + ) -> mouse::Interaction { + self.scene.viewcube_mouse_interaction(state) + } +} + +// ── ViewportPane shader::Program impl ──────────────────────────────────── impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for ViewportPane<'a> { type State = CameraState;