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 <noreply@anthropic.com>
This commit is contained in:
parent
fda0f2dbb9
commit
a41a05baa1
5 changed files with 149 additions and 57 deletions
|
|
@ -669,12 +669,6 @@ impl OpenCADStudio {
|
||||||
Message::SetRenderMode(mode) => {
|
Message::SetRenderMode(mode) => {
|
||||||
use acadrust::entities::ViewportRenderMode as M;
|
use acadrust::entities::ViewportRenderMode as M;
|
||||||
let i = self.active_tab;
|
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 {
|
let label = match mode {
|
||||||
M::Wireframe2D => "Wireframe 2D",
|
M::Wireframe2D => "Wireframe 2D",
|
||||||
M::Wireframe3D => "Wireframe 3D",
|
M::Wireframe3D => "Wireframe 3D",
|
||||||
|
|
@ -684,6 +678,21 @@ impl OpenCADStudio {
|
||||||
M::FlatShadedWithEdges => "Flat Shaded + Edges",
|
M::FlatShadedWithEdges => "Flat Shaded + Edges",
|
||||||
M::GouraudShadedWithEdges => "Gouraud 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();
|
self.tabs[i].visual_style = label.into();
|
||||||
// Re-upload face3d fills on the next frame — the render
|
// Re-upload face3d fills on the next frame — the render
|
||||||
// pipeline keys its upload cache off `geometry_epoch`.
|
// pipeline keys its upload cache off `geometry_epoch`.
|
||||||
|
|
|
||||||
116
src/app/view.rs
116
src/app/view.rs
|
|
@ -387,49 +387,7 @@ impl OpenCADStudio {
|
||||||
// 3D face / mesh fills and edges are rendered for the active
|
// 3D face / mesh fills and edges are rendered for the active
|
||||||
// tab. Hatch fills are deliberately *not* gated by this — the
|
// tab. Hatch fills are deliberately *not* gated by this — the
|
||||||
// document's FILLMODE still owns 2D fill state.
|
// document's FILLMODE still owns 2D fill state.
|
||||||
let render_modes: Vec<RenderModeChoice> = vec![
|
let info = render_mode_picker(tab.render_mode);
|
||||||
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 viewport_mouse = mouse_area(container(
|
let viewport_mouse = mouse_area(container(
|
||||||
iced::widget::Space::new().width(Fill).height(Fill),
|
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)
|
.width(Fill)
|
||||||
.height(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)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
@ -2017,6 +1991,60 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme
|
||||||
.into()
|
.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 ─────────────────────────────────────────
|
// ── Dynamic-input field formatting ─────────────────────────────────────────
|
||||||
|
|
||||||
/// Short prefix shown before a dynamic-input box's value.
|
/// Short prefix shown before a dynamic-input box's value.
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// View-rotation matrix for the active viewport (MSPACE), or the
|
||||||
/// paper-space camera's matrix when not in MSPACE.
|
/// paper-space camera's matrix when not in MSPACE.
|
||||||
/// Used by ViewCube hit-testing so clicks map to the correct camera.
|
/// Used by ViewCube hit-testing so clicks map to the correct camera.
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,7 @@ impl Scene {
|
||||||
vp_handle: Handle,
|
vp_handle: Handle,
|
||||||
hover_region: Option<usize>,
|
hover_region: Option<usize>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
show_viewcube: bool,
|
||||||
) -> Primitive {
|
) -> Primitive {
|
||||||
let render_mode = match self.document.get_entity(vp_handle) {
|
let render_mode = match self.document.get_entity(vp_handle) {
|
||||||
Some(EntityType::Viewport(vp)) => vp.render_mode,
|
Some(EntityType::Viewport(vp)) => vp.render_mode,
|
||||||
|
|
@ -551,7 +552,7 @@ impl Scene {
|
||||||
cam_rotation: cam.view_rotation_mat(),
|
cam_rotation: cam.view_rotation_mat(),
|
||||||
hover_region,
|
hover_region,
|
||||||
bg_color: self.bg_color,
|
bg_color: self.bg_color,
|
||||||
show_viewcube: false,
|
show_viewcube,
|
||||||
fill_mode: self.document.header.fill_mode,
|
fill_mode: self.document.header.fill_mode,
|
||||||
view_wireframe,
|
view_wireframe,
|
||||||
mesh_fill: flags.mesh_fill,
|
mesh_fill: flags.mesh_fill,
|
||||||
|
|
@ -570,8 +571,10 @@ impl Scene {
|
||||||
hover_region: Option<usize>,
|
hover_region: Option<usize>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> PaperViewportPrimitive {
|
) -> PaperViewportPrimitive {
|
||||||
|
// The active (double-clicked) viewport shows the ViewCube gizmo,
|
||||||
|
// mirroring the model-space view.
|
||||||
PaperViewportPrimitive(
|
PaperViewportPrimitive(
|
||||||
self.build_viewport_primitive(vp_handle, hover_region, bounds),
|
self.build_viewport_primitive(vp_handle, hover_region, bounds, true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::render::{CameraState, PaperViewportPrimitive, Primitive};
|
use super::render::{CameraState, PaperViewportPrimitive, Primitive};
|
||||||
use super::Scene;
|
use super::{Scene, VIEWCUBE_PX};
|
||||||
|
use crate::app::Message;
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use iced::widget::shader;
|
use iced::widget::shader;
|
||||||
use iced::{mouse, Event, Rectangle};
|
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 State = CameraState;
|
||||||
type Primitive = PaperViewportPrimitive;
|
type Primitive = PaperViewportPrimitive;
|
||||||
|
|
||||||
|
|
@ -90,9 +91,30 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for PaperViewportPan
|
||||||
event: &Event,
|
event: &Event,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
) -> Option<iced::widget::Action<Msg>> {
|
) -> Option<iced::widget::Action<Message>> {
|
||||||
self.scene.update_viewcube_state(state, bounds, cursor);
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,7 +149,7 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||||
),
|
),
|
||||||
ViewportPaneMode::Paper { handle } => {
|
ViewportPaneMode::Paper { handle } => {
|
||||||
self.scene
|
self.scene
|
||||||
.build_viewport_primitive(*handle, state.hover_region, bounds)
|
.build_viewport_primitive(*handle, state.hover_region, bounds, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue