refactor(viewport): unify paper into one shader, transparent clear
Collapse the paper-space rendering path into the same shader widget the model layout uses. The PaperCanvas widget draws the paper sheet, paper entities and viewport borders underneath; the unified shader paints all content viewports on top, scissored to each viewport's rect. - Drop PaperViewportPane / PaperViewportPipeline newtype / PaperViewportPrimitive, ViewportPaneMode::Paper, ViewportPane::paper, build_primitive and build_viewport_primitive. Scene exposes only build_viewports. - view.rs: the paper branch stacks PaperCanvas + the unified shader instead of the old paper_canvas_view function. The active paper viewport's blue border, render-mode picker and ViewCube hit-area are layered in the same viewport_stack as the model layout. - Transparent shader clear (alpha=0) + alpha-blended blit pipeline so the underlying widget (container bg in model, PaperCanvas sheet in paper) shows through everywhere the shader has not painted geometry — including inside content viewport rectangles. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
417a708275
commit
8d64426087
5 changed files with 91 additions and 455 deletions
136
src/app/view.rs
136
src/app/view.rs
|
|
@ -5,7 +5,7 @@ use super::document::{DynComponent, DynFieldEntry};
|
||||||
use super::{Message, OpenCADStudio};
|
use super::{Message, OpenCADStudio};
|
||||||
use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
|
use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
|
||||||
use crate::scene::paper_canvas::PaperCanvas;
|
use crate::scene::paper_canvas::PaperCanvas;
|
||||||
use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane};
|
use crate::scene::viewport_pane::ViewportPane;
|
||||||
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
||||||
use crate::ui::overlay;
|
use crate::ui::overlay;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
|
|
@ -253,10 +253,13 @@ impl OpenCADStudio {
|
||||||
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
||||||
// returned here also flags the rest of `view` to skip drawing-only
|
// returned here also flags the rest of `view` to skip drawing-only
|
||||||
// overlays via `tab.is_start`.
|
// overlays via `tab.is_start`.
|
||||||
|
// Unified GPU widget for both layouts. In a paper layout the
|
||||||
|
// PaperCanvas (2-D sheet + paper entities + viewport borders) is
|
||||||
|
// layered underneath this shader so the area outside floating
|
||||||
|
// viewports shows the sheet; the shader only blits inside each
|
||||||
|
// content viewport's scissor rect.
|
||||||
let viewport_3d: Element<'_, Message> = if tab.is_start {
|
let viewport_3d: Element<'_, Message> = if tab.is_start {
|
||||||
start_page_view()
|
start_page_view()
|
||||||
} else if is_paper {
|
|
||||||
paper_canvas_view(tab)
|
|
||||||
} else {
|
} else {
|
||||||
shader(ViewportPane::model(
|
shader(ViewportPane::model(
|
||||||
&tab.scene,
|
&tab.scene,
|
||||||
|
|
@ -457,6 +460,21 @@ impl OpenCADStudio {
|
||||||
.height(Fill)]
|
.height(Fill)]
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
|
} else if is_paper {
|
||||||
|
// Paper layout: 2-D sheet + paper entities + viewport borders
|
||||||
|
// underneath, unified GPU shader on top (only paints inside
|
||||||
|
// each content viewport's scissor rect so the sheet shows
|
||||||
|
// through outside them).
|
||||||
|
let paper_sheet: Element<'_, Message> =
|
||||||
|
canvas(PaperCanvas::new(&tab.scene)).width(Fill).height(Fill).into();
|
||||||
|
stack![
|
||||||
|
paper_sheet,
|
||||||
|
container(viewport_3d).width(Fill).height(Fill),
|
||||||
|
selection_overlay,
|
||||||
|
viewport_mouse,
|
||||||
|
]
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
} else {
|
} else {
|
||||||
stack![
|
stack![
|
||||||
container(viewport_3d)
|
container(viewport_3d)
|
||||||
|
|
@ -559,6 +577,35 @@ impl OpenCADStudio {
|
||||||
if let Some(rect) = active_vp_rect {
|
if let Some(rect) = active_vp_rect {
|
||||||
let x = rect.x.max(0.0);
|
let x = rect.x.max(0.0);
|
||||||
let y = rect.y.max(0.0);
|
let y = rect.y.max(0.0);
|
||||||
|
// Highlight the active viewport with a 2-px border so its
|
||||||
|
// boundary is always visible over the GPU shader.
|
||||||
|
const VP_BORDER: Color = Color {
|
||||||
|
r: 0.18,
|
||||||
|
g: 0.52,
|
||||||
|
b: 0.95,
|
||||||
|
a: 1.0,
|
||||||
|
};
|
||||||
|
let border_frame = container(
|
||||||
|
Space::new()
|
||||||
|
.width(iced::Length::Fixed(rect.width.max(1.0)))
|
||||||
|
.height(iced::Length::Fixed(rect.height.max(1.0))),
|
||||||
|
)
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
border: iced::Border {
|
||||||
|
color: VP_BORDER,
|
||||||
|
width: 2.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let border_layer = column![
|
||||||
|
Space::new().height(iced::Length::Fixed(y)),
|
||||||
|
row![Space::new().width(iced::Length::Fixed(x)), border_frame,],
|
||||||
|
]
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill);
|
||||||
|
viewport_stack = viewport_stack.push(border_layer);
|
||||||
|
|
||||||
let vp_mode = tab
|
let vp_mode = tab
|
||||||
.scene
|
.scene
|
||||||
.active_viewport_render_mode()
|
.active_viewport_render_mode()
|
||||||
|
|
@ -932,89 +979,6 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Paper canvas ──────────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// PSPACE: single full-canvas PaperSheet widget — renders paper entities plus
|
|
||||||
// model content of all viewports via CPU projection.
|
|
||||||
//
|
|
||||||
// MSPACE (active viewport): PaperSheet widget (excludes the active viewport
|
|
||||||
// from its CPU projection) + a PaperViewportPane widget overlaid at the
|
|
||||||
// active viewport's screen-space position. PaperViewportPane uses a
|
|
||||||
// distinct pipeline type (PaperViewportPipeline) so Iced's per-type storage
|
|
||||||
// keeps the two prepare() calls from overwriting each other.
|
|
||||||
|
|
||||||
fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, Message> {
|
|
||||||
let scene = &tab.scene;
|
|
||||||
|
|
||||||
// 2-D canvas for the paper sheet — paper entities, viewport borders, and
|
|
||||||
// inactive viewport projections are rendered as vector paths. This lets
|
|
||||||
// users select/edit paper-space entities directly without entering MSPACE.
|
|
||||||
let paper_sheet = canvas(PaperCanvas::new(scene)).width(Fill).height(Fill);
|
|
||||||
|
|
||||||
if let Some(vp_handle) = scene.active_viewport {
|
|
||||||
let (canvas_w, canvas_h) = scene.selection.borrow().vp_size;
|
|
||||||
if let Some(rect) = scene.viewport_screen_rect(vp_handle, (canvas_w, canvas_h)) {
|
|
||||||
// Clamp to canvas bounds so Space widgets never get negative size.
|
|
||||||
let x = rect.x.max(0.0).min(canvas_w);
|
|
||||||
let y = rect.y.max(0.0).min(canvas_h);
|
|
||||||
let w = rect.width.clamp(1.0, canvas_w - x);
|
|
||||||
let h = rect.height.clamp(1.0, canvas_h - y);
|
|
||||||
|
|
||||||
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle))
|
|
||||||
.width(iced::Length::Fixed(w))
|
|
||||||
.height(iced::Length::Fixed(h));
|
|
||||||
|
|
||||||
let positioned = column![
|
|
||||||
Space::new().height(iced::Length::Fixed(y)),
|
|
||||||
row![Space::new().width(iced::Length::Fixed(x)), vp_widget,],
|
|
||||||
]
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill);
|
|
||||||
|
|
||||||
// Blue border drawn on top of the 3-D overlay so the viewport
|
|
||||||
// boundary is always visible even when the shader fills the area.
|
|
||||||
const VP_BORDER: Color = Color {
|
|
||||||
r: 0.18,
|
|
||||||
g: 0.52,
|
|
||||||
b: 0.95,
|
|
||||||
a: 1.0,
|
|
||||||
};
|
|
||||||
let border_frame = container(
|
|
||||||
Space::new()
|
|
||||||
.width(iced::Length::Fixed(w))
|
|
||||||
.height(iced::Length::Fixed(h)),
|
|
||||||
)
|
|
||||||
.style(move |_: &Theme| container::Style {
|
|
||||||
border: iced::Border {
|
|
||||||
color: VP_BORDER,
|
|
||||||
width: 2.0,
|
|
||||||
radius: 0.0.into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let border_layer = column![
|
|
||||||
Space::new().height(iced::Length::Fixed(y)),
|
|
||||||
row![Space::new().width(iced::Length::Fixed(x)), border_frame,],
|
|
||||||
]
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill);
|
|
||||||
|
|
||||||
// The render-mode picker and ViewCube hit area for the active
|
|
||||||
// viewport are layered in the main view stack (above the
|
|
||||||
// viewport mouse_area) so they're clickable — see
|
|
||||||
// `view()`. Here we only draw the sheet, the 3-D content, and
|
|
||||||
// the highlight border.
|
|
||||||
return stack![paper_sheet, positioned, border_layer]
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill)
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paper_sheet.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Document tab bar ───────────────────────────────────────────────────────
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
//! entities (title blocks, annotations, viewport borders) can be interacted
|
//! entities (title blocks, annotations, viewport borders) can be interacted
|
||||||
//! with directly — no need to enter MSPACE just to click on them.
|
//! with directly — no need to enter MSPACE just to click on them.
|
||||||
//!
|
//!
|
||||||
//! When MSPACE is active, a separate `PaperViewportPane` shader widget is
|
//! A single unified `ViewportPane` shader widget is stacked on top of this
|
||||||
//! stacked on top of this canvas for the active viewport's 3-D content
|
//! canvas to render every content viewport's 3-D model content into its own
|
||||||
//! (see `view.rs::paper_canvas_view()`).
|
//! scissor rectangle — outside those rectangles the shader does not paint,
|
||||||
|
//! so the paper sheet drawn here shows through.
|
||||||
|
|
||||||
use iced::widget::canvas;
|
use iced::widget::canvas;
|
||||||
use iced::{mouse, Color, Point, Rectangle};
|
use iced::{mouse, Color, Point, Rectangle};
|
||||||
|
|
|
||||||
|
|
@ -772,7 +772,12 @@ impl Pipeline {
|
||||||
entry_point: Some("fs_main"),
|
entry_point: Some("fs_main"),
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format,
|
format,
|
||||||
blend: None,
|
// Alpha-blend the resolve texture onto the surface so a
|
||||||
|
// transparent shader clear (alpha=0 outside any drawn
|
||||||
|
// geometry) leaves whatever the underlying widget
|
||||||
|
// painted — model container bg in a model layout,
|
||||||
|
// PaperCanvas sheet in a paper layout — visible.
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
|
|
||||||
|
|
@ -16,59 +16,6 @@ use super::pipeline::MultiPipeline;
|
||||||
use super::tess_util;
|
use super::tess_util;
|
||||||
use super::{HatchModel, ImageModel, MeshLodSet, Scene, Uniforms, ViewportInstance, WireModel};
|
use super::{HatchModel, ImageModel, MeshLodSet, Scene, Uniforms, ViewportInstance, WireModel};
|
||||||
|
|
||||||
// ── PaperViewportPipeline / PaperViewportPrimitive ────────────────────────
|
|
||||||
//
|
|
||||||
// Newtype wrappers around MultiPipeline / Primitive so that the active-MSPACE
|
|
||||||
// viewport widget gets its own Iced storage entry (keyed by TypeId of the
|
|
||||||
// Pipeline type). This prevents the shared-pipeline prepare() overwrite
|
|
||||||
// that occurs when PaperSheet and the viewport widget both use the same type.
|
|
||||||
|
|
||||||
/// Dedicated pipeline for the MSPACE active-viewport shader widget.
|
|
||||||
pub struct PaperViewportPipeline(pub(super) MultiPipeline);
|
|
||||||
|
|
||||||
impl iced::widget::shader::Pipeline for PaperViewportPipeline {
|
|
||||||
fn new(
|
|
||||||
device: &iced::wgpu::Device,
|
|
||||||
queue: &iced::wgpu::Queue,
|
|
||||||
format: iced::wgpu::TextureFormat,
|
|
||||||
) -> Self {
|
|
||||||
Self(<MultiPipeline as iced::widget::shader::Pipeline>::new(
|
|
||||||
device, queue, format,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Primitive returned by `PaperViewportPane`; delegates everything to the
|
|
||||||
/// inner `Primitive` via the dedicated `PaperViewportPipeline`.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PaperViewportPrimitive(pub(super) Primitive);
|
|
||||||
|
|
||||||
impl shader::Primitive for PaperViewportPrimitive {
|
|
||||||
type Pipeline = PaperViewportPipeline;
|
|
||||||
|
|
||||||
fn prepare(
|
|
||||||
&self,
|
|
||||||
pipeline: &mut PaperViewportPipeline,
|
|
||||||
device: &iced::wgpu::Device,
|
|
||||||
queue: &iced::wgpu::Queue,
|
|
||||||
bounds: &Rectangle,
|
|
||||||
viewport: &Viewport,
|
|
||||||
) {
|
|
||||||
self.0
|
|
||||||
.prepare(&mut pipeline.0, device, queue, bounds, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(
|
|
||||||
&self,
|
|
||||||
pipeline: &PaperViewportPipeline,
|
|
||||||
encoder: &mut iced::wgpu::CommandEncoder,
|
|
||||||
target: &iced::wgpu::TextureView,
|
|
||||||
clip: &Rectangle<u32>,
|
|
||||||
) {
|
|
||||||
self.0.render(&pipeline.0, encoder, target, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|
@ -78,15 +25,6 @@ pub struct CameraState {
|
||||||
|
|
||||||
// ── GPU primitive ─────────────────────────────────────────────────────────
|
// ── GPU primitive ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Normalized rectangle covering the whole widget — used for a single
|
|
||||||
/// full-window viewport (Model view or active paper viewport).
|
|
||||||
const FULL_VIEWPORT_RECT: Rectangle = Rectangle {
|
|
||||||
x: 0.0,
|
|
||||||
y: 0.0,
|
|
||||||
width: 1.0,
|
|
||||||
height: 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Everything needed to render one viewport: its geometry, camera, render
|
/// Everything needed to render one viewport: its geometry, camera, render
|
||||||
/// mode, and the screen rectangle it occupies. The unified renderer carries
|
/// mode, and the screen rectangle it occupies. The unified renderer carries
|
||||||
/// a `Vec<ViewportData>` (one per tiled / floating viewport); each gets its
|
/// a `Vec<ViewportData>` (one per tiled / floating viewport); each gets its
|
||||||
|
|
@ -482,74 +420,6 @@ pub(crate) fn adapt_to_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
|
||||||
// ── Primitive builder helpers (called by ViewportPane's shader::Program impl) ──
|
// ── Primitive builder helpers (called by ViewportPane's shader::Program impl) ──
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
/// Build a full-scene Primitive for the model or paper view (the camera
|
|
||||||
/// stored in `self.camera` is used as-is).
|
|
||||||
pub(super) fn build_primitive(
|
|
||||||
&self,
|
|
||||||
hover_region: Option<usize>,
|
|
||||||
bounds: Rectangle,
|
|
||||||
show_viewcube: bool,
|
|
||||||
render_mode: acadrust::entities::ViewportRenderMode,
|
|
||||||
) -> Primitive {
|
|
||||||
let flags = render_mode_flags(render_mode);
|
|
||||||
let view_wireframe = !flags.face3d_fill;
|
|
||||||
let cam = self.camera.borrow();
|
|
||||||
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
|
|
||||||
// Record the active widget's aspect so view_world_aabb() can compute
|
|
||||||
// a correct culling rectangle before entity_wires_arc() runs.
|
|
||||||
if bounds.height > 0.0 {
|
|
||||||
self.set_render_aspect(bounds.width / bounds.height);
|
|
||||||
self.set_render_pixel_scale(bounds.width, bounds.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
let entity_arc = self.entity_wires_arc();
|
|
||||||
let (face3d_wires, other_wires) = split_face3d_wires(&entity_arc, &self.document);
|
|
||||||
let all_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
|
|
||||||
Arc::new(other_wires)
|
|
||||||
} else {
|
|
||||||
let mut v = other_wires;
|
|
||||||
if let Some(iw) = &self.interim_wire {
|
|
||||||
v.push(iw.clone());
|
|
||||||
}
|
|
||||||
v.extend(self.preview_wires.iter().cloned());
|
|
||||||
Arc::new(v)
|
|
||||||
};
|
|
||||||
|
|
||||||
let bg_color = if self.current_layout == "Model" {
|
|
||||||
self.bg_color
|
|
||||||
} else {
|
|
||||||
self.paper_bg_color
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut uniforms = Uniforms::new(&cam, bounds, self.document.header.lineweight_display);
|
|
||||||
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
|
|
||||||
|
|
||||||
let data = ViewportData {
|
|
||||||
wires: all_wires,
|
|
||||||
face3d_wires: Arc::new(face3d_wires),
|
|
||||||
hatches: self.hatch_models_arc(),
|
|
||||||
wipeout_hatches: self.wipeout_models_arc(),
|
|
||||||
images: self.images_arc(),
|
|
||||||
meshes: self.meshes_arc(),
|
|
||||||
uniforms,
|
|
||||||
cam_rotation: cam.view_rotation_mat(),
|
|
||||||
hover_region,
|
|
||||||
show_viewcube,
|
|
||||||
fill_mode: self.document.header.fill_mode,
|
|
||||||
view_wireframe,
|
|
||||||
mesh_fill: flags.mesh_fill,
|
|
||||||
show_3d_edges: flags.show_3d_edges,
|
|
||||||
hidden_line: flags.hidden_line,
|
|
||||||
geometry_epoch: self.geometry_epoch,
|
|
||||||
camera_generation: self.camera_generation,
|
|
||||||
screen_rect: FULL_VIEWPORT_RECT,
|
|
||||||
};
|
|
||||||
Primitive {
|
|
||||||
viewports: vec![data],
|
|
||||||
bg_color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the unified multi-viewport `Primitive` for the current layout.
|
/// Build the unified multi-viewport `Primitive` for the current layout.
|
||||||
/// Model layout → one full-window viewport (more once tiled); paper
|
/// Model layout → one full-window viewport (more once tiled); paper
|
||||||
/// layout → one viewport per floating content viewport. Each entry is
|
/// layout → one viewport per floating content viewport. Each entry is
|
||||||
|
|
@ -571,40 +441,20 @@ impl Scene {
|
||||||
}
|
}
|
||||||
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
|
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
|
||||||
let instances = self.active_viewports(canvas.0, canvas.1, model_render_mode);
|
let instances = self.active_viewports(canvas.0, canvas.1, model_render_mode);
|
||||||
let bg_color = if self.current_layout == "Model" {
|
// Transparent clear — outside drawn geometry the resolve texture
|
||||||
self.bg_color
|
// stays at alpha=0, so the alpha-blended blit reveals whatever the
|
||||||
} else {
|
// underlying widget painted (model layout: container bg; paper
|
||||||
self.paper_bg_color
|
// layout: PaperCanvas sheet — including inside content viewport
|
||||||
};
|
// rectangles).
|
||||||
|
let bg_color = [0.0, 0.0, 0.0, 0.0];
|
||||||
let viewports: Vec<ViewportData> = instances
|
let viewports: Vec<ViewportData> = instances
|
||||||
.iter()
|
.iter()
|
||||||
.map(|inst| self.viewport_data_for(inst, canvas, hover_region))
|
.map(|inst| self.viewport_data_for(inst, canvas, hover_region))
|
||||||
.collect();
|
.collect();
|
||||||
Primitive {
|
// Empty viewports → blit nothing. The widget container's
|
||||||
viewports: if viewports.is_empty() {
|
// background (model bg or the PaperCanvas widget below) stays
|
||||||
// Paper layout with no content viewports still needs a
|
// visible. This happens in paper layouts without content viewports.
|
||||||
// (blank) pass so the widget clears to the sheet colour.
|
Primitive { viewports, bg_color }
|
||||||
vec![self.viewport_data_for(
|
|
||||||
&ViewportInstance {
|
|
||||||
handle: acadrust::Handle::NULL,
|
|
||||||
screen_rect: Rectangle {
|
|
||||||
x: 0.0,
|
|
||||||
y: 0.0,
|
|
||||||
width: canvas.0,
|
|
||||||
height: canvas.1,
|
|
||||||
},
|
|
||||||
camera: self.camera.borrow().clone(),
|
|
||||||
render_mode: model_render_mode,
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
canvas,
|
|
||||||
None,
|
|
||||||
)]
|
|
||||||
} else {
|
|
||||||
viewports
|
|
||||||
},
|
|
||||||
bg_color,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build one `ViewportData` from a `ViewportInstance`: gathers the
|
/// Build one `ViewportData` from a `ViewportInstance`: gathers the
|
||||||
|
|
@ -678,89 +528,6 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a Primitive that renders model-space content through a specific
|
|
||||||
/// paper-space viewport's camera, applying its layer-freeze list. The
|
|
||||||
/// render mode is read from the viewport entity itself (each paper-space
|
|
||||||
/// viewport carries its own visual style) — the model-space pick_list
|
|
||||||
/// only governs the Model layout.
|
|
||||||
pub(super) fn build_viewport_primitive(
|
|
||||||
&self,
|
|
||||||
vp_handle: Handle,
|
|
||||||
_hover_region: Option<usize>,
|
|
||||||
bounds: Rectangle,
|
|
||||||
show_viewcube: bool,
|
|
||||||
) -> Primitive {
|
|
||||||
// See `build_viewports` — the cube hit-area overlay shadows the
|
|
||||||
// shader, so hover is sourced from the scene cell.
|
|
||||||
let hover_region = self.viewcube_hover.get();
|
|
||||||
let render_mode = match self.document.get_entity(vp_handle) {
|
|
||||||
Some(EntityType::Viewport(vp)) => vp.render_mode,
|
|
||||||
_ => acadrust::entities::ViewportRenderMode::Wireframe2D,
|
|
||||||
};
|
|
||||||
let flags = render_mode_flags(render_mode);
|
|
||||||
let view_wireframe = !flags.face3d_fill;
|
|
||||||
let cam = match self.camera_for_viewport(vp_handle) {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return self.build_primitive(hover_region, bounds, false, render_mode),
|
|
||||||
};
|
|
||||||
|
|
||||||
let base_arc = self.model_wires_for_viewport_arc(vp_handle);
|
|
||||||
let (face3d_wires, other_wires) = split_face3d_wires(&base_arc, &self.document);
|
|
||||||
let all_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
|
|
||||||
Arc::new(other_wires)
|
|
||||||
} else {
|
|
||||||
let mut v = other_wires;
|
|
||||||
if let Some(iw) = &self.interim_wire {
|
|
||||||
v.push(iw.clone());
|
|
||||||
}
|
|
||||||
v.extend(self.preview_wires.iter().cloned());
|
|
||||||
Arc::new(v)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut uniforms = Uniforms::new(&cam, bounds, self.document.header.lineweight_display);
|
|
||||||
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
|
|
||||||
|
|
||||||
let data = ViewportData {
|
|
||||||
wires: all_wires,
|
|
||||||
face3d_wires: Arc::new(face3d_wires),
|
|
||||||
hatches: self.hatch_models_arc(),
|
|
||||||
wipeout_hatches: self.wipeout_models_arc(),
|
|
||||||
images: self.images_arc(),
|
|
||||||
meshes: self.meshes_arc(),
|
|
||||||
uniforms,
|
|
||||||
cam_rotation: cam.view_rotation_mat(),
|
|
||||||
hover_region,
|
|
||||||
show_viewcube,
|
|
||||||
fill_mode: self.document.header.fill_mode,
|
|
||||||
view_wireframe,
|
|
||||||
mesh_fill: flags.mesh_fill,
|
|
||||||
show_3d_edges: flags.show_3d_edges,
|
|
||||||
hidden_line: flags.hidden_line,
|
|
||||||
geometry_epoch: self.geometry_epoch,
|
|
||||||
camera_generation: self.camera_generation,
|
|
||||||
screen_rect: FULL_VIEWPORT_RECT,
|
|
||||||
};
|
|
||||||
Primitive {
|
|
||||||
viewports: vec![data],
|
|
||||||
bg_color: self.bg_color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap `build_viewport_primitive()` in `PaperViewportPrimitive` for use
|
|
||||||
/// by `PaperViewportPane`, which needs its own dedicated pipeline type.
|
|
||||||
pub(super) fn build_active_viewport_primitive(
|
|
||||||
&self,
|
|
||||||
vp_handle: Handle,
|
|
||||||
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, true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update viewcube hover state from cursor position within `bounds`.
|
/// Update viewcube hover state from cursor position within `bounds`.
|
||||||
///
|
///
|
||||||
/// The cube draws in the top-right of the *active model tile* (which fills
|
/// The cube draws in the top-right of the *active model tile* (which fills
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,23 @@
|
||||||
use super::render::{CameraState, PaperViewportPrimitive, Primitive};
|
//! Unified GPU shader widget for both model and paper layouts.
|
||||||
use super::{Scene, VIEWCUBE_PX};
|
//!
|
||||||
use crate::app::Message;
|
//! There is one `shader::Program` for the whole canvas. `Scene::build_viewports`
|
||||||
use acadrust::Handle;
|
//! decides what to render — model layout produces one viewport per tile
|
||||||
|
//! (`active_viewports` iterates `model_tiles`), paper layout produces one per
|
||||||
|
//! content viewport entity. Each one is drawn into its own scissor rect by a
|
||||||
|
//! dedicated inner `Pipeline` (kept in the `MultiPipeline` outer).
|
||||||
|
|
||||||
|
use super::render::{CameraState, Primitive};
|
||||||
|
use super::Scene;
|
||||||
use iced::widget::shader;
|
use iced::widget::shader;
|
||||||
use iced::{mouse, Event, Rectangle};
|
use iced::{mouse, Event, Rectangle};
|
||||||
|
|
||||||
// ── Mode ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub enum ViewportPaneMode {
|
|
||||||
/// Full model space — fills whatever bounds Iced assigns.
|
|
||||||
Model,
|
|
||||||
/// Model-space content rendered through a specific viewport's 3-D camera.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
Paper { handle: Handle },
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Widget struct ─────────────────────────────────────────────────────────
|
// ── Widget struct ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct ViewportPane<'a> {
|
pub struct ViewportPane<'a> {
|
||||||
pub scene: &'a Scene,
|
pub scene: &'a Scene,
|
||||||
pub mode: ViewportPaneMode,
|
|
||||||
pub show_viewcube: bool,
|
pub show_viewcube: bool,
|
||||||
/// Render mode for the **Model** layout view. Paper-space viewports
|
/// Render mode applied to the Model layout's tiles. Paper-space content
|
||||||
/// read their own `render_mode` field from the viewport entity, so
|
/// viewports use the render mode stored on their own viewport entity.
|
||||||
/// this value is ignored when `mode` is `Paper`.
|
|
||||||
pub render_mode: acadrust::entities::ViewportRenderMode,
|
pub render_mode: acadrust::entities::ViewportRenderMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,100 +29,13 @@ impl<'a> ViewportPane<'a> {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
scene,
|
scene,
|
||||||
mode: ViewportPaneMode::Model,
|
|
||||||
show_viewcube,
|
show_viewcube,
|
||||||
render_mode,
|
render_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One paper-space viewport: model content rendered through its own camera.
|
|
||||||
/// See [`ViewportPaneMode::Paper`] for why this is currently unused.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn paper(scene: &'a Scene, handle: Handle) -> Self {
|
|
||||||
Self {
|
|
||||||
scene,
|
|
||||||
mode: ViewportPaneMode::Paper { handle },
|
|
||||||
show_viewcube: false,
|
|
||||||
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PaperViewportPane ─────────────────────────────────────────────────────
|
// ── shader::Program impl ──────────────────────────────────────────────────
|
||||||
//
|
|
||||||
// A shader widget for the MSPACE active viewport. Uses PaperViewportPrimitive
|
|
||||||
// (and therefore PaperViewportPipeline) so it gets its own Iced storage entry,
|
|
||||||
// separate from the ViewportPane/PaperSheet pipeline.
|
|
||||||
|
|
||||||
pub struct PaperViewportPane<'a> {
|
|
||||||
pub scene: &'a Scene,
|
|
||||||
pub handle: Handle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PaperViewportPane<'a> {
|
|
||||||
pub fn new(scene: &'a Scene, handle: Handle) -> Self {
|
|
||||||
Self { scene, handle }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> shader::Program<Message> for PaperViewportPane<'a> {
|
|
||||||
type State = CameraState;
|
|
||||||
type Primitive = PaperViewportPrimitive;
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
state: &Self::State,
|
|
||||||
_cursor: mouse::Cursor,
|
|
||||||
bounds: Rectangle,
|
|
||||||
) -> Self::Primitive {
|
|
||||||
self.scene
|
|
||||||
.build_active_viewport_primitive(self.handle, state.hover_region, bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
|
||||||
&self,
|
|
||||||
state: &mut Self::State,
|
|
||||||
event: &Event,
|
|
||||||
bounds: Rectangle,
|
|
||||||
cursor: mouse::Cursor,
|
|
||||||
) -> Option<iced::widget::Action<Message>> {
|
|
||||||
self.scene.update_viewcube_state(state, bounds, cursor);
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mouse_interaction(
|
|
||||||
&self,
|
|
||||||
state: &Self::State,
|
|
||||||
_b: Rectangle,
|
|
||||||
_c: mouse::Cursor,
|
|
||||||
) -> mouse::Interaction {
|
|
||||||
self.scene.viewcube_mouse_interaction(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── ViewportPane shader::Program impl ────────────────────────────────────
|
|
||||||
|
|
||||||
impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a> {
|
impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a> {
|
||||||
type State = CameraState;
|
type State = CameraState;
|
||||||
|
|
@ -140,19 +47,8 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Self::Primitive {
|
) -> Self::Primitive {
|
||||||
match &self.mode {
|
self.scene
|
||||||
// Unified multi-viewport build: Model layout yields one
|
.build_viewports(bounds, self.render_mode, state.hover_region)
|
||||||
// full-window viewport (more once tiled); the camera, render
|
|
||||||
// mode and ViewCube come from `active_viewports`.
|
|
||||||
ViewportPaneMode::Model => {
|
|
||||||
self.scene
|
|
||||||
.build_viewports(bounds, self.render_mode, state.hover_region)
|
|
||||||
}
|
|
||||||
ViewportPaneMode::Paper { handle } => {
|
|
||||||
self.scene
|
|
||||||
.build_viewport_primitive(*handle, state.hover_region, bounds, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
|
|
@ -162,8 +58,11 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
) -> Option<iced::widget::Action<Msg>> {
|
) -> Option<iced::widget::Action<Msg>> {
|
||||||
// ViewCube hover only makes sense in the full model-space view.
|
// ViewCube hover is also driven from the app-level CursorMoved /
|
||||||
if matches!(self.mode, ViewportPaneMode::Model) && self.show_viewcube {
|
// ViewportMove handlers (the cube hit-area overlay shadows this
|
||||||
|
// widget for those events). Keeping the call here gives a fallback
|
||||||
|
// path while the cursor is over the bare shader.
|
||||||
|
if self.show_viewcube {
|
||||||
self.scene.update_viewcube_state(state, bounds, cursor);
|
self.scene.update_viewcube_state(state, bounds, cursor);
|
||||||
} else {
|
} else {
|
||||||
state.hover_region = None;
|
state.hover_region = None;
|
||||||
|
|
@ -178,7 +77,7 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||||
_b: Rectangle,
|
_b: Rectangle,
|
||||||
_c: mouse::Cursor,
|
_c: mouse::Cursor,
|
||||||
) -> mouse::Interaction {
|
) -> mouse::Interaction {
|
||||||
if matches!(self.mode, ViewportPaneMode::Model) && self.show_viewcube {
|
if self.show_viewcube {
|
||||||
self.scene.viewcube_mouse_interaction(state)
|
self.scene.viewcube_mouse_interaction(state)
|
||||||
} else {
|
} else {
|
||||||
mouse::Interaction::default()
|
mouse::Interaction::default()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue