From 9382a412dfa286f3f0b6542a61dfc9081c789c67 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 31 Jul 2026 23:47:41 +0300 Subject: [PATCH] fix(viewport): adapt layout crosshair contrast Choose the sheet or desk surface under the cursor instead of the active input space so MSPACE stays visible on light paper.\n\nRefs #611 --- src/app/view/mod.rs | 40 +++++++++++++++++++++++++++++++++++----- src/scene/limits.rs | 3 +++ src/scene/mod.rs | 7 +++++++ src/scene/paper.rs | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 8bd2802a0..5655a8cfb 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -39,6 +39,40 @@ const PAPER_SPACE_BACKGROUND: Color = Color { a: 1.0, }; +/// Base surface directly under the crosshair. Paper content viewports render +/// transparently over the sheet/desk, including while MSPACE input is active. +fn crosshair_background(tab: &DocumentTab, is_paper: bool) -> [f32; 4] { + if !is_paper { + return tab.scene.bg_color; + } + + let desk = [ + PAPER_SPACE_BACKGROUND.r, + PAPER_SPACE_BACKGROUND.g, + PAPER_SPACE_BACKGROUND.b, + PAPER_SPACE_BACKGROUND.a, + ]; + let (cursor, viewport_size) = { + let selection = tab.scene.selection.borrow(); + (selection.last_move_pos, selection.vp_size) + }; + let Some(cursor) = cursor else { + return desk; + }; + if viewport_size.0 <= 0.0 || viewport_size.1 <= 0.0 { + return desk; + } + let on_sheet = tab + .scene + .paper_sheet_screen_rect(viewport_size) + .is_some_and(|rect| rect.contains(cursor)); + if on_sheet { + tab.scene.paper_bg_color + } else { + desk + } +} + /// Clear gap (px) kept between the render-mode bar (top-left) and the ViewCube /// (top-right) before the cube is judged to collide and hides. const VIEWCUBE_GAP: f32 = 12.0; @@ -592,11 +626,7 @@ impl OpenCADStudio { tab.pan_mode, self.ribbon.open_dropdown.is_some(), hover_locked, - if tab.scene.input_uses_model_space() { - tab.scene.bg_color - } else { - tab.scene.paper_bg_color - }, + crosshair_background(tab, is_paper), ) }; diff --git a/src/scene/limits.rs b/src/scene/limits.rs index 22cd7f825..b6b18daa8 100644 --- a/src/scene/limits.rs +++ b/src/scene/limits.rs @@ -118,6 +118,9 @@ impl Scene { } } } + self.paper_viewport_cache + .borrow_mut() + .remove(&self.current_layout); } /// ZOOM All frames the configured drawing limits. Object-only framing diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f11b6e825..e70e76cd1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -458,6 +458,7 @@ struct PaperViewportCache { layout_block: Handle, sheet: Handle, content: Arc>, + paper_limits: Option<((f64, f64), (f64, f64))>, } struct PaperSheetRenderCache { @@ -3354,6 +3355,12 @@ impl Scene { return None; } + if let Some(cache) = self.paper_viewport_cache.borrow().get(&self.current_layout) { + if cache.epoch == self.geometry_epoch && cache.layout == self.current_layout { + return cache.paper_limits; + } + } + self.document .objects .values() diff --git a/src/scene/paper.rs b/src/scene/paper.rs index fe22c740d..061659c3f 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -20,6 +20,7 @@ impl Scene { let layout_block = self.current_layout_block_handle(); let sheet = self.current_layout_sheet_viewport_handle(); + let paper_limits = self.paper_limits(); let is_content = |handle: Handle| { let Some(EntityType::Viewport(vp)) = self.document.get_entity(handle) else { return false; @@ -62,6 +63,7 @@ impl Scene { layout_block, sheet, content: Arc::clone(&content), + paper_limits, }, ); (layout_block, sheet, content) @@ -324,6 +326,43 @@ impl Scene { height: h, }) } + + /// Physical sheet bounds in canvas pixels. Uses the same forced top-down + /// paper transform as the GPU sheet viewport, ignoring stored camera twist. + pub fn paper_sheet_screen_rect( + &self, + canvas_px: (f32, f32), + ) -> Option { + let ((x0, y0), (x1, y1)) = self.paper_limits()?; + let (canvas_w, canvas_h) = canvas_px; + if canvas_w < 1.0 || canvas_h < 1.0 { + return None; + } + + let cam = self.camera.borrow(); + let half_h = cam.ortho_size(); + let half_w = half_h * canvas_w / canvas_h; + let tx = cam.target.x as f32; + let ty = cam.target.y as f32; + drop(cam); + 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 min_x = x0.min(x1) as f32; + let max_x = x0.max(x1) as f32; + let min_y = y0.min(y1) as f32; + let max_y = y0.max(y1) as f32; + let (left, top) = to_px(min_x, max_y); + let (right, bottom) = to_px(max_x, min_y); + Some(iced::Rectangle { + x: left, + y: top, + width: (right - left).max(0.0), + height: (bottom - top).max(0.0), + }) + } // ── Paper-space helpers ─────────────────────────────────────────────── /// Paper-layout hatch fills, restricted to the active layout block (used by