From fcc29de4693814f6eac0c53d76b78731eddb11d4 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Apr 2026 13:57:34 +0300 Subject: [PATCH] feat: inline MSPACE viewport overlay on 2D paper canvas - viewport_screen_rect now mirrors PaperCanvas::draw()'s camera-based to_px transform instead of using a fixed paper-limits scale, so the 3D overlay lands exactly over the drawn viewport border at any zoom/pan - blue border frame (stack layer above the shader) makes the active viewport boundary always visible even when it fills the canvas - space-mode pill in MSPACE now dispatches LayoutSwitch("Model") so clicking it jumps to full Model Space instead of just exiting to PSPACE Co-Authored-By: Claude Sonnet 4.6 --- src/app/view.rs | 38 ++++++++++++++++++++++++++++++----- src/scene/mod.rs | 48 +++++++++++++++++++++++++-------------------- src/ui/statusbar.rs | 6 +++--- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/app/view.rs b/src/app/view.rs index 0bf9480a..366abe50 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -481,10 +481,11 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M 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); + // Clamp to canvas bounds so Space widgets never get negative size. + let x = rect.x.max(0.0).min(canvas_w); + let y = rect.y.max(0.0).min(canvas_h); + let w = rect.width.clamp(1.0, canvas_w - x); + let h = rect.height.clamp(1.0, canvas_h - y); let vp_widget = shader(PaperViewportPane::new(scene, vp_handle)) .width(iced::Length::Fixed(w)) @@ -500,7 +501,34 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M .width(Fill) .height(Fill); - return stack![paper_sheet, positioned] + // Blue border drawn on top of the 3-D overlay so the viewport + // boundary is always visible even when the shader fills the area. + const VP_BORDER: Color = Color { r: 0.18, g: 0.52, b: 0.95, a: 1.0 }; + let border_frame = container( + Space::new() + .width(iced::Length::Fixed(w)) + .height(iced::Length::Fixed(h)), + ) + .style(move |_: &Theme| container::Style { + border: iced::Border { + color: VP_BORDER, + width: 2.0, + radius: 0.0.into(), + }, + ..Default::default() + }); + + let border_layer = column![ + Space::new().height(iced::Length::Fixed(y)), + row![ + Space::new().width(iced::Length::Fixed(x)), + border_frame, + ], + ] + .width(Fill) + .height(Fill); + + return stack![paper_sheet, positioned, border_layer] .width(Fill) .height(Fill) .into(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 4226cee0..2f75fe80 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2169,45 +2169,51 @@ impl Scene { /// Convert a paper-space Viewport entity's position/size into a pixel /// `Rectangle` relative to the top-left of the paper canvas. /// - /// `canvas_px` — the pixel dimensions of the area that shows the paper. - /// Map a paper-space viewport into screen-pixel coordinates. - /// Used to position the active-viewport widget in MSPACE paper space. + /// Uses the same camera-based ortho transform as `PaperCanvas::draw()` so + /// that the overlay lands exactly over the drawn viewport border regardless + /// of zoom or pan level. pub fn viewport_screen_rect( &self, vp_handle: Handle, canvas_px: (f32, f32), ) -> Option { - let ((px0, py0), (px1, py1)) = self.paper_limits()?; - let paper_w = (px1 - px0) as f32; - let paper_h = (py1 - py0) as f32; - if paper_w < 1e-6 || paper_h < 1e-6 { - return None; - } - let vp = match self.document.get_entity(vp_handle) { Some(EntityType::Viewport(vp)) => vp, _ => return None, }; let (canvas_w, canvas_h) = canvas_px; - let sx = canvas_w / paper_w; - let sy = canvas_h / paper_h; + if canvas_w < 1.0 || canvas_h < 1.0 { + return None; + } + + let cam = self.camera.borrow(); + let aspect = canvas_w / canvas_h; + let half_h = cam.ortho_size(); + let half_w = half_h * aspect; + let tx = cam.target.x; + let ty = cam.target.y; + drop(cam); + + // Mirror the to_px closure in PaperCanvas::draw(). + let to_px = |wx: f32, wy: f32| -> (f32, f32) { + let x = (wx - tx + half_w) / (2.0 * half_w) * canvas_w; + let y = (ty + half_h - wy) / (2.0 * half_h) * canvas_h; + (x, y) + }; let cx = vp.center.x as f32; let cy = vp.center.y as f32; let hw = (vp.width / 2.0) as f32; let hh = (vp.height / 2.0) as f32; - // Map paper coords (origin = paper min, Y up) → screen (Y down). - let screen_x = (cx - hw - px0 as f32) * sx; - let screen_y = (py1 as f32 - (cy + hh)) * sy; + let (x0, y0) = to_px(cx - hw, cy + hh); // top-left in screen + let (x1, y1) = to_px(cx + hw, cy - hh); // bottom-right in screen - Some(iced::Rectangle { - x: screen_x, - y: screen_y, - width: vp.width as f32 * sx, - height: vp.height as f32 * sy, - }) + let w = (x1 - x0).max(1.0); + let h = (y1 - y0).max(1.0); + + Some(iced::Rectangle { x: x0, y: y0, width: w, height: h }) } // ── ViewportPane helpers ────────────────────────────────────────────── diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index dd8d01c5..3568f72c 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -92,7 +92,7 @@ impl StatusBar { osnap_btn(osnap_active, snapper.snap_enabled, popup_open), tip( space_mode_btn(¤t_layout, in_mspace), - "Toggle Model/Paper space\nDouble-click viewport to enter MSPACE", + "PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space", ), status_pill(scale_label), ] @@ -460,9 +460,9 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes // PAPER = currently in paper-space editing // MODEL = currently in model-space editing (either the Model tab or MSPACE) let (label, active, on_press) = if is_model_tab { - ("MODEL", false, None) + ("MODEL", false, None::) } else if in_mspace { - ("MODEL", true, Some(Message::ExitViewport)) + ("MODEL", true, Some(Message::LayoutSwitch("Model".to_string()))) } else { ("PAPER", false, Some(Message::MspaceCommand)) };