feat: inline MSPACE viewport overlay on 2D paper canvas
- viewport_screen_rect now mirrors PaperCanvas::draw()'s camera-based
to_px transform instead of using a fixed paper-limits scale, so the
3D overlay lands exactly over the drawn viewport border at any zoom/pan
- blue border frame (stack layer above the shader) makes the active
viewport boundary always visible even when it fills the canvas
- space-mode pill in MSPACE now dispatches LayoutSwitch("Model") so
clicking it jumps to full Model Space instead of just exiting to PSPACE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
459b525ffe
commit
fcc29de469
3 changed files with 63 additions and 29 deletions
|
|
@ -481,10 +481,11 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M
|
||||||
if let Some(vp_handle) = scene.active_viewport {
|
if let Some(vp_handle) = scene.active_viewport {
|
||||||
let (canvas_w, canvas_h) = scene.selection.borrow().vp_size;
|
let (canvas_w, canvas_h) = scene.selection.borrow().vp_size;
|
||||||
if let Some(rect) = scene.viewport_screen_rect(vp_handle, (canvas_w, canvas_h)) {
|
if let Some(rect) = scene.viewport_screen_rect(vp_handle, (canvas_w, canvas_h)) {
|
||||||
let w = rect.width.max(1.0);
|
// Clamp to canvas bounds so Space widgets never get negative size.
|
||||||
let h = rect.height.max(1.0);
|
let x = rect.x.max(0.0).min(canvas_w);
|
||||||
let x = rect.x.max(0.0);
|
let y = rect.y.max(0.0).min(canvas_h);
|
||||||
let y = rect.y.max(0.0);
|
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))
|
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle))
|
||||||
.width(iced::Length::Fixed(w))
|
.width(iced::Length::Fixed(w))
|
||||||
|
|
@ -500,7 +501,34 @@ 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]
|
// 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);
|
||||||
|
|
||||||
|
return stack![paper_sheet, positioned, border_layer]
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
|
||||||
|
|
@ -2169,45 +2169,51 @@ impl Scene {
|
||||||
/// Convert a paper-space Viewport entity's position/size into a pixel
|
/// Convert a paper-space Viewport entity's position/size into a pixel
|
||||||
/// `Rectangle` relative to the top-left of the paper canvas.
|
/// `Rectangle` relative to the top-left of the paper canvas.
|
||||||
///
|
///
|
||||||
/// `canvas_px` — the pixel dimensions of the area that shows the paper.
|
/// Uses the same camera-based ortho transform as `PaperCanvas::draw()` so
|
||||||
/// Map a paper-space viewport into screen-pixel coordinates.
|
/// that the overlay lands exactly over the drawn viewport border regardless
|
||||||
/// Used to position the active-viewport widget in MSPACE paper space.
|
/// of zoom or pan level.
|
||||||
pub fn viewport_screen_rect(
|
pub fn viewport_screen_rect(
|
||||||
&self,
|
&self,
|
||||||
vp_handle: Handle,
|
vp_handle: Handle,
|
||||||
canvas_px: (f32, f32),
|
canvas_px: (f32, f32),
|
||||||
) -> Option<iced::Rectangle> {
|
) -> Option<iced::Rectangle> {
|
||||||
let ((px0, py0), (px1, py1)) = self.paper_limits()?;
|
|
||||||
let paper_w = (px1 - px0) as f32;
|
|
||||||
let paper_h = (py1 - py0) as f32;
|
|
||||||
if paper_w < 1e-6 || paper_h < 1e-6 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let vp = match self.document.get_entity(vp_handle) {
|
let vp = match self.document.get_entity(vp_handle) {
|
||||||
Some(EntityType::Viewport(vp)) => vp,
|
Some(EntityType::Viewport(vp)) => vp,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (canvas_w, canvas_h) = canvas_px;
|
let (canvas_w, canvas_h) = canvas_px;
|
||||||
let sx = canvas_w / paper_w;
|
if canvas_w < 1.0 || canvas_h < 1.0 {
|
||||||
let sy = canvas_h / paper_h;
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cam = self.camera.borrow();
|
||||||
|
let aspect = canvas_w / canvas_h;
|
||||||
|
let half_h = cam.ortho_size();
|
||||||
|
let half_w = half_h * aspect;
|
||||||
|
let tx = cam.target.x;
|
||||||
|
let ty = cam.target.y;
|
||||||
|
drop(cam);
|
||||||
|
|
||||||
|
// Mirror the to_px closure in PaperCanvas::draw().
|
||||||
|
let to_px = |wx: f32, wy: f32| -> (f32, f32) {
|
||||||
|
let x = (wx - tx + half_w) / (2.0 * half_w) * canvas_w;
|
||||||
|
let y = (ty + half_h - wy) / (2.0 * half_h) * canvas_h;
|
||||||
|
(x, y)
|
||||||
|
};
|
||||||
|
|
||||||
let cx = vp.center.x as f32;
|
let cx = vp.center.x as f32;
|
||||||
let cy = vp.center.y as f32;
|
let cy = vp.center.y as f32;
|
||||||
let hw = (vp.width / 2.0) as f32;
|
let hw = (vp.width / 2.0) as f32;
|
||||||
let hh = (vp.height / 2.0) as f32;
|
let hh = (vp.height / 2.0) as f32;
|
||||||
|
|
||||||
// Map paper coords (origin = paper min, Y up) → screen (Y down).
|
let (x0, y0) = to_px(cx - hw, cy + hh); // top-left in screen
|
||||||
let screen_x = (cx - hw - px0 as f32) * sx;
|
let (x1, y1) = to_px(cx + hw, cy - hh); // bottom-right in screen
|
||||||
let screen_y = (py1 as f32 - (cy + hh)) * sy;
|
|
||||||
|
|
||||||
Some(iced::Rectangle {
|
let w = (x1 - x0).max(1.0);
|
||||||
x: screen_x,
|
let h = (y1 - y0).max(1.0);
|
||||||
y: screen_y,
|
|
||||||
width: vp.width as f32 * sx,
|
Some(iced::Rectangle { x: x0, y: y0, width: w, height: h })
|
||||||
height: vp.height as f32 * sy,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ViewportPane helpers ──────────────────────────────────────────────
|
// ── ViewportPane helpers ──────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ impl StatusBar {
|
||||||
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
||||||
tip(
|
tip(
|
||||||
space_mode_btn(¤t_layout, in_mspace),
|
space_mode_btn(¤t_layout, in_mspace),
|
||||||
"Toggle Model/Paper space\nDouble-click viewport to enter MSPACE",
|
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
||||||
),
|
),
|
||||||
status_pill(scale_label),
|
status_pill(scale_label),
|
||||||
]
|
]
|
||||||
|
|
@ -460,9 +460,9 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes
|
||||||
// PAPER = currently in paper-space editing
|
// PAPER = currently in paper-space editing
|
||||||
// MODEL = currently in model-space editing (either the Model tab or MSPACE)
|
// MODEL = currently in model-space editing (either the Model tab or MSPACE)
|
||||||
let (label, active, on_press) = if is_model_tab {
|
let (label, active, on_press) = if is_model_tab {
|
||||||
("MODEL", false, None)
|
("MODEL", false, None::<Message>)
|
||||||
} else if in_mspace {
|
} else if in_mspace {
|
||||||
("MODEL", true, Some(Message::ExitViewport))
|
("MODEL", true, Some(Message::LayoutSwitch("Model".to_string())))
|
||||||
} else {
|
} else {
|
||||||
("PAPER", false, Some(Message::MspaceCommand))
|
("PAPER", false, Some(Message::MspaceCommand))
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue