feat(paper): render-mode picker + ViewCube on active viewport

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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-27 18:05:09 +03:00
1 parent fda0f2dbb9
commit a41a05baa1
5 files changed
+149 -57

No files matched your search

+15 -6
View File
@@ -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`.
+72 -44
View File
@@ -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<RenderModeChoice> = 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<RenderModeChoice> = 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.
+30
View File
@@ -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<acadrust::entities::ViewportRenderMode> {
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.
+5 -2
View File
@@ -512,6 +512,7 @@ impl Scene {
vp_handle: Handle,
hover_region: Option<usize>,
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<usize>,
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),
)
}
+27 -5
View File
@@ -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<Msg> for PaperViewportPane<'a> {
impl<'a> shader::Program<Message> for PaperViewportPane<'a> {
type State = CameraState;
type Primitive = PaperViewportPrimitive;
@@ -90,9 +91,30 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for PaperViewportPan
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<iced::widget::Action<Msg>> {
) -> Option<iced::widget::Action<Message>> {
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<Msg> 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)
}
}
}