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
This commit is contained in:
Hakan Seven 2026-07-31 23:47:41 +03:00
commit 9382a412df
4 changed files with 84 additions and 5 deletions

View file

@ -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),
)
};

View file

@ -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

View file

@ -458,6 +458,7 @@ struct PaperViewportCache {
layout_block: Handle,
sheet: Handle,
content: Arc<Vec<Handle>>,
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()

View file

@ -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<iced::Rectangle> {
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