fix(paper): make active-viewport picker & ViewCube clickable

Picker ve ViewCube paper_canvas içinde gömülü olduğundan tümüyle
viewport mouse_area'nın altında kalıyor ve tıklamaları o yakalıyordu.
İkisi de ana view stack'ine, mouse_area'nın üstüne taşındı; aktif
viewport'un ekran dikdörtgeninin köşelerine konumlandırılıyor.

- model picker artık yalnızca model layout'ta gösteriliyor (paper'da
  viewport kendi picker'ını alıyor)
- ViewCube hit alanı aktif viewport rect'inin sağ üst köşesinde
- CursorMoved ve ViewportClick, ViewCube hit-test'ini full canvas yerine
  aktif viewport rect'ine göre eşliyor; böylece paper'da gizmo ile
  tıklama hizalı
- picker/viewcube dışı tıklamalar alttaki mouse_area'ya geçtiği için
  viewport içi seçim/çizim etkilenmiyor

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-27 18:17:10 +03:00
commit eeb7df5fec
2 changed files with 112 additions and 38 deletions

View file

@ -1570,11 +1570,26 @@ impl OpenCADStudio {
// ── Cursor / viewport messages ───────────────────────────────── // ── Cursor / viewport messages ─────────────────────────────────
Message::CursorMoved(p) => { Message::CursorMoved(p) => {
let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size; // `p` is relative to the ViewCube hit area's top-left. Map
self.cursor_pos = iced::Point::new( // it back to full-canvas coordinates so ViewportClick's
vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE + p.x, // hit-test lines up. The hit area sits in the top-right of
VIEWCUBE_PAD + p.y, // the full canvas in model space, or of the active
); // viewport's screen rectangle in a paper layout.
let i = self.active_tab;
let (vw, _vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (ox, oy) = match self
.tabs[i]
.scene
.active_viewport
.and_then(|h| self.tabs[i].scene.viewport_screen_rect(h, (vw, _vh)))
{
Some(rect) => (
rect.x + rect.width - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE,
rect.y + VIEWCUBE_PAD,
),
None => (vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE, VIEWCUBE_PAD),
};
self.cursor_pos = iced::Point::new(ox + p.x, oy + p.y);
Task::none() Task::none()
} }
@ -2637,14 +2652,26 @@ impl OpenCADStudio {
let i = self.active_tab; let i = self.active_tab;
let rot = self.tabs[i].scene.active_view_rotation_mat(); let rot = self.tabs[i].scene.active_view_rotation_mat();
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
if let Some(region) = scene::hit_test( // The ViewCube draws in the top-right of whichever area
self.cursor_pos.x, // owns it: the full canvas in model space, or the active
self.cursor_pos.y, // viewport's screen rectangle in a paper layout. Map the
vw, // cursor into that area before hit-testing so paper-space
vh, // picks line up with the gizmo.
rot, let (cx, cy, w, h) = match self
VIEWCUBE_PX, .tabs[i]
) { .scene
.active_viewport
.and_then(|hndl| self.tabs[i].scene.viewport_screen_rect(hndl, (vw, vh)))
{
Some(rect) => (
self.cursor_pos.x - rect.x,
self.cursor_pos.y - rect.y,
rect.width,
rect.height,
),
None => (self.cursor_pos.x, self.cursor_pos.y, vw, vh),
};
if let Some(region) = scene::hit_test(cx, cy, w, h, rot, VIEWCUBE_PX) {
return Task::done(Message::ViewCubeSnap(region)); return Task::done(Message::ViewCubeSnap(region));
} }
Task::none() Task::none()

View file

@ -471,21 +471,79 @@ impl OpenCADStudio {
.height(Fill), .height(Fill),
selection_overlay, selection_overlay,
viewport_mouse, viewport_mouse,
// Toggle sits ABOVE the viewport mouse_area so clicks ]
// inside the toggle's bounds reach it instead of the .width(Fill)
// shader behind it; `opaque` blocks the same clicks from .height(Fill)
// bubbling further down. Outside the toggle's footprint };
// the outer Fill container is transparent — viewport
// drawing / selection stays unaffected. // Model-space render-mode picker, top-left. Sits ABOVE the
// viewport mouse_area so clicks inside its bounds reach it
// instead of the shader behind it; `opaque` stops them bubbling
// further. Outside the chip the Fill container is transparent so
// viewport drawing / selection is unaffected. In a paper layout
// the active viewport gets its own picker (below) instead.
if !is_paper && !tab.is_start {
viewport_stack = viewport_stack.push(
container(iced::widget::opaque(info)) container(iced::widget::opaque(info))
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::alignment::Horizontal::Left) .align_x(iced::alignment::Horizontal::Left)
.align_y(iced::alignment::Vertical::Top), .align_y(iced::alignment::Vertical::Top),
);
}
// Active paper-space viewport overlays: a render-mode picker in
// its top-left corner and a ViewCube hit area in its top-right,
// both layered ABOVE the viewport mouse_area so they receive
// clicks (the paper canvas itself sits below it). Positioned with
// leading Spaces sized to the viewport's screen rectangle.
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
tab.scene.active_viewport.and_then(|h| {
let (cw, ch) = tab.scene.selection.borrow().vp_size;
tab.scene.viewport_screen_rect(h, (cw, ch))
})
} else {
None
};
if let Some(rect) = active_vp_rect {
let x = rect.x.max(0.0);
let y = rect.y.max(0.0);
let vp_mode = tab
.scene
.active_viewport_render_mode()
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D);
let picker_layer = column![
Space::new().height(iced::Length::Fixed(y + 4.0)),
row![
Space::new().width(iced::Length::Fixed(x + 4.0)),
iced::widget::opaque(render_mode_picker(vp_mode)),
],
] ]
.width(Fill) .width(Fill)
.height(Fill) .height(Fill);
}; viewport_stack = viewport_stack.push(picker_layer);
if self.show_viewcube {
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
let cube_click = column![
Space::new().height(iced::Length::Fixed(cube_y)),
row![
Space::new().width(iced::Length::Fixed(cube_x)),
mouse_area(
iced::widget::Space::new()
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
)
.on_move(Message::CursorMoved)
.on_press(Message::ViewportClick),
],
]
.width(Fill)
.height(Fill);
viewport_stack = viewport_stack.push(cube_click);
}
}
if self.show_viewcube && !is_paper && !tab.is_start { if self.show_viewcube && !is_paper && !tab.is_start {
let cube_click: Element<'_, Message> = container( let cube_click: Element<'_, Message> = container(
@ -889,23 +947,12 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M
.width(Fill) .width(Fill)
.height(Fill); .height(Fill);
// Render-mode picker in the active viewport's top-left corner, // The render-mode picker and ViewCube hit area for the active
// mirroring the model-space overlay. Reads / writes the // viewport are layered in the main view stack (above the
// viewport entity's own render mode via SetRenderMode. // viewport mouse_area) so they're clickable — see
let vp_mode = scene // `view()`. Here we only draw the sheet, the 3-D content, and
.active_viewport_render_mode() // the highlight border.
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D); return stack![paper_sheet, positioned, border_layer]
let picker_layer = column![
Space::new().height(iced::Length::Fixed(y + 4.0)),
row![
Space::new().width(iced::Length::Fixed(x + 4.0)),
iced::widget::opaque(render_mode_picker(vp_mode)),
],
]
.width(Fill)
.height(Fill);
return stack![paper_sheet, positioned, border_layer, picker_layer]
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.into(); .into();