feat: render active MSPACE viewport as a true 3D widget in paper space
When a paper-space viewport is activated (double-click / MSPACE command), it is now rendered through its own dedicated PaperViewportPipeline rather than the CPU-projection approach used for inactive viewports. Architecture: - PaperSheet widget (full-canvas): renders paper entities + CPU-projected content of all viewports *except* the active one. - PaperViewportPane widget (Fixed w×h, positioned at viewport screen rect): renders the active viewport with a true 3D camera derived from the viewport's view_direction/view_target/view_height. PaperViewportPipeline is a newtype of Pipeline. Having a distinct type gives it its own Iced storage entry (keyed by TypeId), preventing the shared prepare() overwrite that broke the earlier per-viewport approach. Changes: - render.rs: PaperViewportPipeline, PaperViewportPrimitive newtypes; build_active_viewport_primitive(); build_paper_sheet_primitive() now excludes active_viewport from CPU projection. - viewport_pane.rs: PaperViewportPane<'a> with shader::Program impl. - mod.rs: viewport_content_wires() gains exclude_vp parameter; viewport_screen_rect() is no longer dead code. - view.rs: paper_canvas_view() overlays PaperViewportPane when MSPACE is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d1d4bb3d02
commit
97dbd80c93
4 changed files with 171 additions and 21 deletions
|
|
@ -4,7 +4,7 @@ use super::history::history_dropdown_labels;
|
|||
use super::helpers::grid_plane_from_camera;
|
||||
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
||||
use crate::scene::grip::grips_to_screen;
|
||||
use crate::scene::viewport_pane::ViewportPane;
|
||||
use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane};
|
||||
use crate::ui::overlay;
|
||||
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row, Space};
|
||||
use iced::window;
|
||||
|
|
@ -457,22 +457,52 @@ impl H7CAD {
|
|||
|
||||
// ── Paper canvas ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Renders the full paper canvas as a single full-size shader widget using the
|
||||
// PaperSheet mode, which includes both paper-space entities (title blocks,
|
||||
// frames, borders) and model-space content projected through each viewport's
|
||||
// view matrix into paper-space coordinates.
|
||||
// PSPACE: single full-canvas PaperSheet widget — renders paper entities plus
|
||||
// model content of all viewports via CPU projection.
|
||||
//
|
||||
// Note: A per-viewport widget approach (ViewportPane::Paper) was attempted but
|
||||
// does not work correctly because Iced 0.14 batches all shader prepare() calls
|
||||
// before any render() calls, causing widgets that share the same Pipeline type
|
||||
// to overwrite each other's GPU buffers. The CPU-projection approach used here
|
||||
// is the correct solution within Iced's shader framework.
|
||||
// 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> {
|
||||
shader(ViewportPane::paper_sheet(&tab.scene))
|
||||
let scene = &tab.scene;
|
||||
|
||||
let paper_sheet = shader(ViewportPane::paper_sheet(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)) {
|
||||
let w = rect.width.max(1.0);
|
||||
let h = rect.height.max(1.0);
|
||||
let x = rect.x.max(0.0);
|
||||
let y = rect.y.max(0.0);
|
||||
|
||||
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);
|
||||
|
||||
return stack![paper_sheet, positioned]
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
paper_sheet.into()
|
||||
}
|
||||
|
||||
// ── Document tab bar ───────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ impl Scene {
|
|||
if let Some(((x0, y0), (x1, y1))) = self.paper_limits() {
|
||||
wires.insert(0, paper_boundary_wire(x0 as f32, y0 as f32, x1 as f32, y1 as f32));
|
||||
}
|
||||
wires.extend(self.viewport_content_wires(layout_block, None));
|
||||
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
||||
}
|
||||
wires
|
||||
}
|
||||
|
|
@ -375,7 +375,7 @@ impl Scene {
|
|||
}
|
||||
Some(vp_handle) => {
|
||||
// MSPACE: only model content visible through the active viewport
|
||||
self.viewport_content_wires(layout_block, Some(vp_handle))
|
||||
self.viewport_content_wires(layout_block, Some(vp_handle), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -667,7 +667,12 @@ impl Scene {
|
|||
|
||||
/// Collect model-space wires projected into paper space for all (or one specific)
|
||||
/// user viewports. `only_vp = Some(h)` restricts output to that viewport.
|
||||
fn viewport_content_wires(&self, paper_block: Handle, only_vp: Option<Handle>) -> Vec<WireModel> {
|
||||
fn viewport_content_wires(
|
||||
&self,
|
||||
paper_block: Handle,
|
||||
only_vp: Option<Handle>,
|
||||
exclude_vp: Option<Handle>,
|
||||
) -> Vec<WireModel> {
|
||||
use acadrust::entities::Viewport;
|
||||
use std::collections::HashSet as HSet;
|
||||
|
||||
|
|
@ -682,6 +687,7 @@ impl Scene {
|
|||
&& vp.common.owner_handle == paper_block
|
||||
&& vp.status.is_on
|
||||
&& only_vp.map_or(true, |h| vp.common.handle == h)
|
||||
&& exclude_vp.map_or(true, |h| vp.common.handle != h)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
@ -2104,8 +2110,7 @@ impl Scene {
|
|||
///
|
||||
/// `canvas_px` — the pixel dimensions of the area that shows the paper.
|
||||
/// Map a paper-space viewport into screen-pixel coordinates.
|
||||
/// Needed when per-viewport shader widgets are revived (see `ViewportPaneMode::Paper`).
|
||||
#[allow(dead_code)]
|
||||
/// Used to position the active-viewport widget in MSPACE paper space.
|
||||
pub fn viewport_screen_rect(
|
||||
&self,
|
||||
vp_handle: Handle,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,56 @@ use super::pipeline::Pipeline;
|
|||
use super::tessellate;
|
||||
use super::{HatchModel, ImageModel, MeshModel, Scene, Uniforms, WireModel};
|
||||
|
||||
// ── PaperViewportPipeline / PaperViewportPrimitive ────────────────────────
|
||||
//
|
||||
// Newtype wrappers around Pipeline / 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 `Pipeline`.
|
||||
|
||||
/// Dedicated pipeline for the MSPACE active-viewport shader widget.
|
||||
pub struct PaperViewportPipeline(pub(super) Pipeline);
|
||||
|
||||
impl iced::widget::shader::Pipeline for PaperViewportPipeline {
|
||||
fn new(
|
||||
device: &iced::wgpu::Device,
|
||||
queue: &iced::wgpu::Queue,
|
||||
format: iced::wgpu::TextureFormat,
|
||||
) -> Self {
|
||||
Self(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) ───────────────────────────
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
|
@ -209,7 +259,9 @@ impl Scene {
|
|||
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
let mut wires = self.paper_sheet_wires();
|
||||
wires.extend(self.viewport_content_wires(layout_block, None));
|
||||
// When MSPACE is active, exclude that viewport from the CPU projection —
|
||||
// it is rendered in 3D by the separate PaperViewportPane widget.
|
||||
wires.extend(self.viewport_content_wires(layout_block, None, self.active_viewport));
|
||||
if let Some(iw) = &self.interim_wire {
|
||||
wires.push(iw.clone());
|
||||
}
|
||||
|
|
@ -260,6 +312,17 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
PaperViewportPrimitive(self.build_viewport_primitive(vp_handle, hover_region, bounds))
|
||||
}
|
||||
|
||||
/// Update viewcube hover state from cursor position within `bounds`.
|
||||
pub(super) fn update_viewcube_state(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use super::render::{CameraState, Primitive};
|
||||
use super::render::{CameraState, PaperViewportPrimitive, Primitive};
|
||||
use super::Scene;
|
||||
use acadrust::Handle;
|
||||
use iced::widget::shader;
|
||||
|
|
@ -48,7 +48,59 @@ impl<'a> ViewportPane<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// ── shader::Program impl ──────────────────────────────────────────────────
|
||||
// ── PaperViewportPane ─────────────────────────────────────────────────────
|
||||
//
|
||||
// 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, Msg: std::fmt::Debug + Clone> shader::Program<Msg> 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<Msg>> {
|
||||
self.scene.update_viewcube_state(state, bounds, cursor);
|
||||
let _ = event;
|
||||
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> {
|
||||
type State = CameraState;
|
||||
|
|
|
|||
Loading…
Reference in a new issue