From a41a05baa10d4f0f17e02f907a1c3c55192d13ff Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 27 May 2026 18:05:09 +0300 Subject: [PATCH] feat(paper): render-mode picker + ViewCube on active viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paper-space'de bir viewport'a çift tıklanıp aktif hale gelince, model space'deki gibi sol üst köşede render-mode dropdown'ı ve sağ üst köşede ViewCube gösteriliyor. - render_mode_picker helper'a çıkarıldı; model space ve aktif paper viewport ortak kullanıyor (kod tekrarı yok) - SetRenderMode artık viewport-aware: aktif viewport varsa o viewport entity'sinin render_mode'unu, yoksa model-layout tab stilini günceller (Scene::active_viewport_render_mode getter/setter) - build_viewport_primitive'e show_viewcube parametresi; aktif viewport primitive'i ViewCube gizmo'sunu çiziyor - PaperViewportPane shader::Program artık Message'a özelleşti ve sol tık ViewCube hit-test'ini bounds-relative yapıp ViewCubeSnap üretiyor Bilinen sorun: picker/viewcube doğru konumda görünüyor ancak tıklamalar üstteki viewport_mouse mouse_area katmanına gidiyor; katman sırası bir sonraki adımda düzeltilecek. Co-Authored-By: Claude Opus 4.7 --- src/app/update.rs | 21 +++++-- src/app/view.rs | 116 +++++++++++++++++++++++-------------- src/scene/mod.rs | 30 ++++++++++ src/scene/render.rs | 7 ++- src/scene/viewport_pane.rs | 32 ++++++++-- 5 files changed, 149 insertions(+), 57 deletions(-) diff --git a/src/app/update.rs b/src/app/update.rs index 38eb13efc..4ae8139b1 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -669,12 +669,6 @@ impl OpenCADStudio { Message::SetRenderMode(mode) => { use acadrust::entities::ViewportRenderMode as M; let i = self.active_tab; - self.tabs[i].render_mode = mode; - // Keep the legacy `wireframe` bool synced — both wireframe - // modes set it, everything else clears it. - let wf = matches!(mode, M::Wireframe2D | M::Wireframe3D); - self.tabs[i].wireframe = wf; - self.ribbon.set_wireframe(wf); let label = match mode { M::Wireframe2D => "Wireframe 2D", M::Wireframe3D => "Wireframe 3D", @@ -684,6 +678,21 @@ impl OpenCADStudio { M::FlatShadedWithEdges => "Flat Shaded + Edges", M::GouraudShadedWithEdges => "Gouraud Shaded + Edges", }; + // In a paper layout with an active (double-clicked) + // viewport, the picker drives that viewport entity's own + // render mode; the model-layout tab style is untouched. + if self.tabs[i].scene.set_active_viewport_render_mode(mode) { + self.tabs[i].scene.bump_geometry(); + self.command_line + .push_output(&format!("Viewport visual style: {label}")); + return Task::none(); + } + self.tabs[i].render_mode = mode; + // Keep the legacy `wireframe` bool synced — both wireframe + // modes set it, everything else clears it. + let wf = matches!(mode, M::Wireframe2D | M::Wireframe3D); + self.tabs[i].wireframe = wf; + self.ribbon.set_wireframe(wf); self.tabs[i].visual_style = label.into(); // Re-upload face3d fills on the next frame — the render // pipeline keys its upload cache off `geometry_epoch`. diff --git a/src/app/view.rs b/src/app/view.rs index e8e1de2c1..75bfa661a 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -387,49 +387,7 @@ impl OpenCADStudio { // 3D face / mesh fills and edges are rendered for the active // tab. Hatch fills are deliberately *not* gated by this — the // document's FILLMODE still owns 2D fill state. - let render_modes: Vec = vec![ - RenderModeChoice(acadrust::entities::ViewportRenderMode::Wireframe2D), - RenderModeChoice(acadrust::entities::ViewportRenderMode::Wireframe3D), - RenderModeChoice(acadrust::entities::ViewportRenderMode::HiddenLine), - RenderModeChoice(acadrust::entities::ViewportRenderMode::FlatShaded), - RenderModeChoice(acadrust::entities::ViewportRenderMode::GouraudShaded), - RenderModeChoice(acadrust::entities::ViewportRenderMode::FlatShadedWithEdges), - RenderModeChoice(acadrust::entities::ViewportRenderMode::GouraudShadedWithEdges), - ]; - let info = container( - iced::widget::pick_list( - render_modes, - Some(RenderModeChoice(tab.render_mode)), - |c| Message::SetRenderMode(c.0), - ) - .text_size(11), - ) - .style(|_: &Theme| iced::widget::container::Style { - background: Some(iced::Background::Color(Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 0.75, - })), - border: iced::Border { - color: Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }, - width: 1.0, - radius: 4.0.into(), - }, - text_color: Some(Color { - r: 0.85, - g: 0.85, - b: 0.85, - a: 1.0, - }), - ..Default::default() - }) - .padding([4, 8]); + let info = render_mode_picker(tab.render_mode); let viewport_mouse = mouse_area(container( iced::widget::Space::new().width(Fill).height(Fill), @@ -931,7 +889,23 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M .width(Fill) .height(Fill); - return stack![paper_sheet, positioned, border_layer] + // Render-mode picker in the active viewport's top-left corner, + // mirroring the model-space overlay. Reads / writes the + // viewport entity's own render mode via SetRenderMode. + let vp_mode = 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) + .height(Fill); + + return stack![paper_sheet, positioned, border_layer, picker_layer] .width(Fill) .height(Fill) .into(); @@ -2017,6 +1991,60 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme .into() } +// ── Render-mode picker ────────────────────────────────────────────────────── + +/// The 7-way visual-style `pick_list`, styled as a dark overlay chip. +/// Shared by the model-space viewport (top-left) and each active +/// paper-space viewport. Emits `SetRenderMode`, which the update loop +/// routes to the active viewport entity or the model-layout tab. +fn render_mode_picker<'a>( + current: acadrust::entities::ViewportRenderMode, +) -> Element<'a, Message> { + use acadrust::entities::ViewportRenderMode as M; + let render_modes: Vec = vec![ + RenderModeChoice(M::Wireframe2D), + RenderModeChoice(M::Wireframe3D), + RenderModeChoice(M::HiddenLine), + RenderModeChoice(M::FlatShaded), + RenderModeChoice(M::GouraudShaded), + RenderModeChoice(M::FlatShadedWithEdges), + RenderModeChoice(M::GouraudShadedWithEdges), + ]; + container( + iced::widget::pick_list(render_modes, Some(RenderModeChoice(current)), |c| { + Message::SetRenderMode(c.0) + }) + .text_size(11), + ) + .style(|_: &Theme| iced::widget::container::Style { + background: Some(iced::Background::Color(Color { + r: 0.10, + g: 0.10, + b: 0.10, + a: 0.75, + })), + border: iced::Border { + color: Color { + r: 0.35, + g: 0.35, + b: 0.35, + a: 1.0, + }, + width: 1.0, + radius: 4.0.into(), + }, + text_color: Some(Color { + r: 0.85, + g: 0.85, + b: 0.85, + a: 1.0, + }), + ..Default::default() + }) + .padding([4, 8]) + .into() +} + // ── Dynamic-input field formatting ───────────────────────────────────────── /// Short prefix shown before a dynamic-input box's value. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 5f9ddd19f..32bcd1cab 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2199,6 +2199,36 @@ impl Scene { } } + /// Render mode of the active paper-space viewport, or `None` when no + /// viewport is active (PSPACE / model layout). + pub fn active_viewport_render_mode( + &self, + ) -> Option { + let h = self.active_viewport?; + match self.document.get_entity(h) { + Some(acadrust::EntityType::Viewport(vp)) => Some(vp.render_mode), + _ => None, + } + } + + /// Set the active paper-space viewport's render mode. Returns `true` + /// when a viewport was active and updated; `false` (no-op) otherwise, + /// so the caller can fall back to the model-layout render mode. + pub fn set_active_viewport_render_mode( + &mut self, + mode: acadrust::entities::ViewportRenderMode, + ) -> bool { + let Some(h) = self.active_viewport else { + return false; + }; + if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(h) { + vp.render_mode = mode; + true + } else { + false + } + } + /// View-rotation matrix for the active viewport (MSPACE), or the /// paper-space camera's matrix when not in MSPACE. /// Used by ViewCube hit-testing so clicks map to the correct camera. diff --git a/src/scene/render.rs b/src/scene/render.rs index 5b6cb4e20..8d49af8ad 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -512,6 +512,7 @@ impl Scene { vp_handle: Handle, hover_region: Option, bounds: Rectangle, + show_viewcube: bool, ) -> Primitive { let render_mode = match self.document.get_entity(vp_handle) { Some(EntityType::Viewport(vp)) => vp.render_mode, @@ -551,7 +552,7 @@ impl Scene { cam_rotation: cam.view_rotation_mat(), hover_region, bg_color: self.bg_color, - show_viewcube: false, + show_viewcube, fill_mode: self.document.header.fill_mode, view_wireframe, mesh_fill: flags.mesh_fill, @@ -570,8 +571,10 @@ impl Scene { hover_region: Option, bounds: Rectangle, ) -> PaperViewportPrimitive { + // The active (double-clicked) viewport shows the ViewCube gizmo, + // mirroring the model-space view. PaperViewportPrimitive( - self.build_viewport_primitive(vp_handle, hover_region, bounds), + self.build_viewport_primitive(vp_handle, hover_region, bounds, true), ) } diff --git a/src/scene/viewport_pane.rs b/src/scene/viewport_pane.rs index 142c65ab0..15d0a7a0c 100644 --- a/src/scene/viewport_pane.rs +++ b/src/scene/viewport_pane.rs @@ -1,5 +1,6 @@ use super::render::{CameraState, PaperViewportPrimitive, Primitive}; -use super::Scene; +use super::{Scene, VIEWCUBE_PX}; +use crate::app::Message; use acadrust::Handle; use iced::widget::shader; use iced::{mouse, Event, Rectangle}; @@ -70,7 +71,7 @@ impl<'a> PaperViewportPane<'a> { } } -impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for PaperViewportPane<'a> { +impl<'a> shader::Program for PaperViewportPane<'a> { type State = CameraState; type Primitive = PaperViewportPrimitive; @@ -90,9 +91,30 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for PaperViewportPan event: &Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Option> { + ) -> Option> { self.scene.update_viewcube_state(state, bounds, cursor); - let _ = event; + // A left click on the ViewCube snaps the viewport's view. The + // hit-test runs in bounds-relative coordinates so it matches the + // gizmo drawn in this viewport's top-right corner. Clicks outside + // the cube return None, so they fall through to the paper sheet + // for normal selection / drawing. + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event { + if let Some(pos) = cursor.position_in(bounds) { + let rot = self.scene.active_view_rotation_mat(); + if let Some(region) = super::hit_test( + pos.x, + pos.y, + bounds.width, + bounds.height, + rot, + VIEWCUBE_PX, + ) { + return Some(iced::widget::Action::publish(Message::ViewCubeSnap( + region, + ))); + } + } + } None } @@ -127,7 +149,7 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for ViewportPane<'a> ), ViewportPaneMode::Paper { handle } => { self.scene - .build_viewport_primitive(*handle, state.hover_region, bounds) + .build_viewport_primitive(*handle, state.hover_region, bounds, false) } } }