From da9fafc0f8f1e708055d38e5c9e59102dbbbd163 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Apr 2026 02:18:41 +0300 Subject: [PATCH] feat: introduce ViewportPane widget for unified model/paper rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Scene: shader::Program impl; all GPU rendering now goes through ViewportPane which supports three modes: Model — full model-space view (unchanged behaviour) PaperSheet — paper-space entities only, using the paper camera Paper — model content through a specific viewport's own camera - Add Scene helper methods (render.rs): build_primitive / build_paper_sheet_primitive / build_viewport_primitive update_viewcube_state / viewcube_mouse_interaction - Add Scene helpers (mod.rs): paper_sheet_wires() — paper entities without viewport projection camera_for_viewport() — derive Camera from Viewport entity data model_wires_for_viewport() — model wires filtered by per-viewport frozen layers viewport_screen_rect() — paper-space → pixel rect mapping - Add paper_canvas_view() in view.rs: Stack of PaperSheet + one Paper widget per Viewport entity, positioned with Space offsets from viewport_screen_rect() - Update VIEWPORT_WIDGET_PLAN.md: mark Steps 1–4 done, document remaining work Co-Authored-By: Claude Sonnet 4.6 --- VIEWPORT_WIDGET_PLAN.md | 143 ++++++++++++++++++++++++ src/app/view.rs | 69 +++++++++++- src/scene/mod.rs | 151 +++++++++++++++++++++++++ src/scene/render.rs | 218 +++++++++++++++++++++++-------------- src/scene/viewport_pane.rs | 94 ++++++++++++++++ 5 files changed, 594 insertions(+), 81 deletions(-) create mode 100644 VIEWPORT_WIDGET_PLAN.md create mode 100644 src/scene/viewport_pane.rs diff --git a/VIEWPORT_WIDGET_PLAN.md b/VIEWPORT_WIDGET_PLAN.md new file mode 100644 index 000000000..7f707eec0 --- /dev/null +++ b/VIEWPORT_WIDGET_PLAN.md @@ -0,0 +1,143 @@ + + +# Unified ViewportPane Widget Plan + +## Current State (as of implementation start) + +- `src/scene/render.rs`: `Scene` implements `shader::Program` → single full-screen GPU widget +- `src/app/view.rs:106`: `shader(&tab.scene).width(Fill).height(Fill)` — same widget for both model and paper space, only render content differs +- `src/entities/viewport.rs`: `Viewport` entity is a pure DXF data struct with no shader or widget of its own + +## Target Architecture + +``` +Model tab → ViewportPane (mode: Model) — Fill × Fill +Paper tab → paper_canvas_view() + ├── ViewportPane (mode: PaperSheet) — Fill × Fill (paper entities + camera) + ├── ViewportPane (mode: Paper, VP1) — vp.width × vp.height px + └── ViewportPane (mode: Paper, VP2) — vp.width × vp.height px +``` + +--- + +## ✅ Step 1 — `ViewportPane` Struct + +**File:** `src/scene/viewport_pane.rs` + +```rust +pub enum ViewportPaneMode { + Model, + PaperSheet, // paper-space entities, paper camera, no viewport projection + Paper { handle: Handle }, // model content through a specific viewport's camera +} +pub struct ViewportPane<'a> { pub scene: &'a Scene, pub mode: ViewportPaneMode } +``` + +Implements `shader::Program`. `Scene`'s own `shader::Program` impl has been **removed** — all rendering now goes through `ViewportPane`. + +Scene helper methods added to `render.rs`: +- `build_primitive()` — model/full paper space render (was `draw()`) +- `build_paper_sheet_primitive()` — paper entities only, no viewport projection +- `build_viewport_primitive(vp_handle)` — model content through viewport camera +- `update_viewcube_state()` / `viewcube_mouse_interaction()` + +Scene helper methods added to `mod.rs`: +- `paper_sheet_wires()` — paper-space entity wires without viewport content +- `camera_for_viewport(handle)` — build Camera from Viewport entity data +- `model_wires_for_viewport(handle)` — model wires filtered by viewport layer freeze + +--- + +## ✅ Step 2 — Per-Viewport Camera + +Implemented via `camera_for_viewport()` in `src/scene/mod.rs`. Derives a `Camera` from the Viewport entity's `view_direction`, `view_target`, and `view_height` each frame — no separate HashMap needed since `pan_active_viewport()` / `zoom_active_viewport()` already write back to the entity. + +--- + +## ✅ Step 3 — Paper-Space Coordinates → Pixel Conversion + +```rust +pub fn viewport_screen_rect(&self, vp_handle: Handle, canvas_px: (f32, f32)) -> Option +``` + +Added to `src/scene/mod.rs`. Uses `paper_limits()` as the paper extent and `scene.selection.borrow().vp_size` as the live canvas size. + +--- + +## ✅ Step 4 — `paper_canvas_view` Function + +Added to `src/app/view.rs`. Builds a `stack![]` with: +1. `shader(ViewportPane::paper_sheet(scene))` — full-size paper background +2. One `shader(ViewportPane::paper(scene, handle))` per viewport, positioned via Space offsets +3. `viewport_3d` in `view()` is now `paper_canvas_view(tab)` when `is_paper` + +--- + +## Step 5 — Mouse Routing per Viewport (TODO) + +Currently the `viewport_mouse` mouse_area covers the full canvas for both model and paper space. MSPACE detection already works via `Scene::active_viewport` + `viewport_at_paper_point()`. What remains: + +- Double-click on a paper viewport → enter MSPACE (set `active_viewport`) +- Escape → exit MSPACE (clear `active_viewport`) +- Pan/zoom in MSPACE → route to `pan_active_viewport()` / `zoom_active_viewport()` + +These are already partially wired — verify that MSPACE interactions reach the correct `ViewportPane::Paper` widget through the existing message handlers. + +--- + +## Step 6 — Per-Viewport Layer Freeze Verification (TODO) + +`build_viewport_primitive()` calls `model_wires_for_viewport()` which filters by `vp.frozen_layers`. Verify that the frozen layer handles match the layer handles in the document (they should since both come from the same `CadDocument`). + +--- + +## Implementation Order + +1. ✅ `ViewportPane` + render helpers — model tab behavior unchanged +2. ✅ Per-viewport camera — derived from entity each frame +3. ✅ `viewport_screen_rect()` — paper → pixel coordinate mapping +4. ✅ `paper_canvas_view()` — stack of PaperSheet + Paper widgets +5. **TODO** Mouse routing verification for MSPACE in paper space +6. **TODO** Per-viewport layer freeze verification + +--- + +## Notes + +- `CameraState` (viewcube hover) is per `ViewportPane` instance — Iced manages this automatically via `shader::Program::State`. +- Paper-space entities (title blocks, frames) are rendered by `ViewportPane::PaperSheet` using the same paper-space camera as before. +- The ViewCube is hidden in `ViewportPane::Paper` mode (only shown in Model and PaperSheet). +- `selection_overlay` and grip markers use paper-space screen coordinates — this is correct for PSPACE. For MSPACE, grips of model entities are projected through the paper camera, which may need adjustment in a future step. +- Known limitation: `viewport_screen_rect()` uses the last-rendered canvas size (`scene.selection.borrow().vp_size`), so viewport positions are correct from the second frame onward. diff --git a/src/app/view.rs b/src/app/view.rs index 20341551d..867cdd635 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -4,6 +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::ui::overlay; use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row, Space}; use iced::window; @@ -103,7 +104,14 @@ impl H7CAD { let i = self.active_tab; let tab = &self.tabs[i]; let is_paper = tab.scene.current_layout != "Model"; - let viewport_3d = shader(&tab.scene).width(Fill).height(Fill); + let viewport_3d: Element<'_, Message> = if is_paper { + paper_canvas_view(tab) + } else { + shader(ViewportPane::model(&tab.scene)) + .width(Fill) + .height(Fill) + .into() + }; let selection_overlay = { let sel = tab.scene.selection.borrow().clone(); @@ -447,6 +455,65 @@ impl H7CAD { } } +// ── Paper canvas ────────────────────────────────────────────────────────── +// +// In paper space, the main canvas is composed of: +// 1. A full-size PaperSheet layer — renders paper-space entities (title blocks, +// frames, borders) with the paper-space camera. +// 2. One ViewportPane::Paper widget per Viewport entity, positioned at its +// paper-space coordinates and sized to its paper-space dimensions. +// Each renders model-space content through its own viewport camera. +// 3. The existing selection_overlay / viewport_mouse / nav stack on top +// (unchanged — mouse event routing stays the same). + +fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, Message> { + let scene = &tab.scene; + + // Layer 1: paper-space entities (title blocks, frames, paper boundary). + let paper_sheet = shader(ViewportPane::paper_sheet(scene)) + .width(Fill) + .height(Fill); + + // Use the last-rendered canvas size to compute pixel positions. + // On the very first frame this is (0,0); correct positions appear from + // the second frame onward (imperceptible in practice). + let (canvas_w, canvas_h) = scene.selection.borrow().vp_size; + let vp_list = scene.viewport_list(); + + let mut layers = stack![paper_sheet]; + + for (handle, _, _) in &vp_list { + let Some(rect) = scene.viewport_screen_rect(*handle, (canvas_w, canvas_h)) else { + continue; + }; + + // Clamp to avoid zero-size or negative widgets. + 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); + + // Layer 2+: one shader widget per viewport, positioned with Space offsets. + let vp_widget = shader(ViewportPane::paper(scene, *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); + + layers = layers.push(positioned); + } + + layers.width(Fill).height(Fill).into() +} + // ── Document tab bar ─────────────────────────────────────────────────────── pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 7024c0721..22286c268 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -18,6 +18,7 @@ pub mod solid3d_tess; pub mod tessellate; pub mod transform; pub mod truck_tess; +pub mod viewport_pane; pub mod wire_model; use camera::Camera; @@ -2095,6 +2096,156 @@ impl Scene { } pub fn update(&mut self, _dt: Duration) {} + + // ── Paper-space coordinate helpers ─────────────────────────────────── + + /// Convert a paper-space Viewport entity's position/size into a pixel + /// `Rectangle` relative to the top-left of the paper canvas. + /// + /// `canvas_px` — the pixel dimensions of the area that shows the paper. + /// Paper limits from the current layout define the paper-space extent; + /// the viewport is mapped linearly into that area (Y-axis flipped). + /// Returns `None` if the viewport handle is invalid or paper limits are missing. + pub fn viewport_screen_rect( + &self, + vp_handle: Handle, + canvas_px: (f32, f32), + ) -> Option { + 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) { + Some(EntityType::Viewport(vp)) => vp, + _ => return None, + }; + + let (canvas_w, canvas_h) = canvas_px; + let sx = canvas_w / paper_w; + let sy = canvas_h / paper_h; + + let cx = vp.center.x as f32; + let cy = vp.center.y as f32; + let hw = (vp.width / 2.0) as f32; + let hh = (vp.height / 2.0) as f32; + + // Map paper coords (origin = paper min, Y up) → screen (Y down). + let screen_x = (cx - hw - px0 as f32) * sx; + let screen_y = (py1 as f32 - (cy + hh)) * sy; + + Some(iced::Rectangle { + x: screen_x, + y: screen_y, + width: vp.width as f32 * sx, + height: vp.height as f32 * sy, + }) + } + + // ── ViewportPane helpers ────────────────────────────────────────────── + + /// Paper-space entity wires only (title blocks, frames, borders). + /// Does NOT include viewport content projection — that is handled by + /// individual ViewportPane::Paper widgets layered on top. + pub(super) fn paper_sheet_wires(&self) -> Vec { + let layout_block = self.current_layout_block_handle(); + let mut wires = self.wires_for_block(layout_block); + 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 + } + + /// Build a Camera oriented and scaled to match a paper-space Viewport entity. + /// Used by `ViewportPane::Paper` to render model-space content through the + /// viewport's own view direction and scale. + fn camera_for_viewport(&self, vp_handle: Handle) -> Option { + let vp = match self.document.get_entity(vp_handle) { + Some(EntityType::Viewport(vp)) => vp, + _ => return None, + }; + + let vd = glam::Vec3::new( + vp.view_direction.x as f32, + vp.view_direction.y as f32, + vp.view_direction.z as f32, + ) + .normalize_or(glam::Vec3::Z); + + let pitch = vd.z.clamp(-0.999, 0.999).asin(); + let yaw = vd.x.atan2(vd.y); + + let target = glam::Vec3::new( + vp.view_target.x as f32, + vp.view_target.y as f32, + vp.view_target.z as f32, + ); + + let fov_y = 45.0_f32.to_radians(); + let view_height = if vp.view_height.abs() > 1e-9 { + vp.view_height as f32 + } else { + vp.height as f32 + }; + // ortho_size = distance * tan(fov_y/2) => distance = view_height/2 / tan(fov_y/2) + let distance = ((view_height / 2.0) / (fov_y * 0.5).tan()).max(0.001); + + Some(camera::Camera { + target, + rotation: camera::yaw_pitch_to_quat(yaw, pitch), + distance, + fov_y, + projection: camera::Projection::Orthographic, + yaw, + pitch, + }) + } + + /// Collect model-space WireModels visible through `vp_handle`, respecting + /// global layer visibility and the viewport's per-viewport layer freeze list. + fn model_wires_for_viewport(&self, vp_handle: Handle) -> Vec { + use std::collections::HashSet as HSet; + + let frozen: HSet = match self.document.get_entity(vp_handle) { + Some(EntityType::Viewport(vp)) => vp.frozen_layers.iter().cloned().collect(), + _ => HSet::new(), + }; + + let model_block = self.model_space_block_handle(); + + self.document + .entities() + .filter(|e| { + let c = e.common(); + if c.invisible || matches!(e, EntityType::Viewport(_)) { + return false; + } + if !self.belongs_to_visible_block(c.handle, c.owner_handle, model_block) { + return false; + } + if self + .document + .layers + .get(&c.layer) + .map(|l| l.flags.off || l.flags.frozen) + .unwrap_or(false) + { + return false; + } + if !frozen.is_empty() { + if let Some(lh) = self.document.layers.get(&c.layer).map(|l| l.handle) { + if frozen.contains(&lh) { + return false; + } + } + } + true + }) + .flat_map(|e| self.tessellate_one(e)) + .collect() + } } impl Default for Scene { diff --git a/src/scene/render.rs b/src/scene/render.rs index 112172264..31a8abdbc 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -3,11 +3,11 @@ use acadrust::tables::LineType; use acadrust::types::{Color as AcadColor, LineWeight}; -use acadrust::EntityType; +use acadrust::{EntityType, Handle}; use glam::Mat4; use iced::mouse; use iced::widget::shader::{self, Viewport}; -use iced::{Event, Rectangle, Size}; +use iced::{Rectangle, Size}; use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX}; use super::pipeline::Pipeline; @@ -40,84 +40,6 @@ pub struct Primitive { pub(super) bg_color: [f32; 4], } -// ── shader::Program impl ────────────────────────────────────────────────── - -impl shader::Program for Scene { - type State = CameraState; - type Primitive = Primitive; - - fn draw( - &self, - state: &Self::State, - _cursor: mouse::Cursor, - bounds: Rectangle, - ) -> Self::Primitive { - let cam = self.camera.borrow(); - self.selection.borrow_mut().vp_size = (bounds.width, bounds.height); - - let mut all_wires = self.entity_wires(); - if let Some(iw) = &self.interim_wire { - all_wires.push(iw.clone()); - } - all_wires.extend(self.preview_wires.iter().cloned()); - - let bg_color = if self.current_layout == "Model" { - self.bg_color - } else { - self.paper_bg_color - }; - - Primitive { - wires: all_wires, - hatches: self.synced_hatch_models(), - wipeout_hatches: self.wipeout_models(), - images: self.images.values().cloned().collect(), - meshes: self.meshes.values().cloned().collect(), - uniforms: Uniforms::new(&cam, bounds), - cam_rotation: cam.view_rotation_mat(), - hover_region: state.hover_region, - bg_color, - } - } - - fn update( - &self, - state: &mut Self::State, - event: &Event, - bounds: Rectangle, - cursor: mouse::Cursor, - ) -> Option> { - let pos = cursor.position_in(bounds); - let cam_rotation = { self.camera.borrow().view_rotation_mat() }; - if let Some(p) = pos { - state.hover_region = hover_id( - p.x, - p.y, - bounds.width, - bounds.height, - cam_rotation, - VIEWCUBE_PX, - ); - } else { - state.hover_region = None; - } - let _ = event; - None - } - - fn mouse_interaction( - &self, - state: &Self::State, - _b: Rectangle, - _c: mouse::Cursor, - ) -> mouse::Interaction { - if state.hover_region.is_some() { - return mouse::Interaction::Pointer; - } - mouse::Interaction::default() - } -} - // ── shader::Primitive impl ──────────────────────────────────────────────── impl shader::Primitive for Primitive { @@ -236,6 +158,142 @@ impl Scene { } } +// ── Primitive builder helpers (called by ViewportPane's shader::Program impl) ── + +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, + bounds: Rectangle, + ) -> Primitive { + let cam = self.camera.borrow(); + self.selection.borrow_mut().vp_size = (bounds.width, bounds.height); + + let mut all_wires = self.entity_wires(); + if let Some(iw) = &self.interim_wire { + all_wires.push(iw.clone()); + } + all_wires.extend(self.preview_wires.iter().cloned()); + + let bg_color = if self.current_layout == "Model" { + self.bg_color + } else { + self.paper_bg_color + }; + + Primitive { + wires: all_wires, + hatches: self.synced_hatch_models(), + wipeout_hatches: self.wipeout_models(), + images: self.images.values().cloned().collect(), + meshes: self.meshes.values().cloned().collect(), + uniforms: Uniforms::new(&cam, bounds), + cam_rotation: cam.view_rotation_mat(), + hover_region, + bg_color, + } + } + + /// Build a Primitive for the paper-sheet background: paper-space entities + /// (title blocks, frames, borders) using the paper-space camera. + /// Viewport content is NOT included — it is rendered by separate + /// `ViewportPane::Paper` widgets stacked on top. + pub(super) fn build_paper_sheet_primitive( + &self, + hover_region: Option, + bounds: Rectangle, + ) -> Primitive { + let cam = self.camera.borrow(); + self.selection.borrow_mut().vp_size = (bounds.width, bounds.height); + + let mut wires = self.paper_sheet_wires(); + if let Some(iw) = &self.interim_wire { + wires.push(iw.clone()); + } + wires.extend(self.preview_wires.iter().cloned()); + + Primitive { + wires, + hatches: self.synced_hatch_models(), + wipeout_hatches: self.wipeout_models(), + images: self.images.values().cloned().collect(), + meshes: self.meshes.values().cloned().collect(), + uniforms: Uniforms::new(&cam, bounds), + cam_rotation: cam.view_rotation_mat(), + hover_region, + bg_color: self.paper_bg_color, + } + } + + /// Build a Primitive that renders model-space content through a specific + /// paper-space viewport's camera, applying its layer-freeze list. + pub(super) fn build_viewport_primitive( + &self, + vp_handle: Handle, + hover_region: Option, + bounds: Rectangle, + ) -> Primitive { + let cam = match self.camera_for_viewport(vp_handle) { + Some(c) => c, + None => return self.build_primitive(hover_region, bounds), + }; + + let mut all_wires = self.model_wires_for_viewport(vp_handle); + if let Some(iw) = &self.interim_wire { + all_wires.push(iw.clone()); + } + all_wires.extend(self.preview_wires.iter().cloned()); + + Primitive { + wires: all_wires, + hatches: self.synced_hatch_models(), + wipeout_hatches: self.wipeout_models(), + images: self.images.values().cloned().collect(), + meshes: self.meshes.values().cloned().collect(), + uniforms: Uniforms::new(&cam, bounds), + cam_rotation: cam.view_rotation_mat(), + hover_region, + bg_color: self.bg_color, + } + } + + /// Update viewcube hover state from cursor position within `bounds`. + pub(super) fn update_viewcube_state( + &self, + state: &mut CameraState, + bounds: Rectangle, + cursor: mouse::Cursor, + ) { + let pos = cursor.position_in(bounds); + let cam_rotation = self.camera.borrow().view_rotation_mat(); + if let Some(p) = pos { + state.hover_region = hover_id( + p.x, + p.y, + bounds.width, + bounds.height, + cam_rotation, + VIEWCUBE_PX, + ); + } else { + state.hover_region = None; + } + } + + pub(super) fn viewcube_mouse_interaction( + &self, + state: &CameraState, + ) -> mouse::Interaction { + if state.hover_region.is_some() { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } +} + // ── Linetype pattern helper ─────────────────────────────────────────────── pub(super) fn resolve_pattern( diff --git a/src/scene/viewport_pane.rs b/src/scene/viewport_pane.rs new file mode 100644 index 000000000..d436b1c49 --- /dev/null +++ b/src/scene/viewport_pane.rs @@ -0,0 +1,94 @@ +use super::render::{CameraState, Primitive}; +use super::Scene; +use acadrust::Handle; +use iced::widget::shader; +use iced::{mouse, Event, Rectangle}; + +// ── Mode ────────────────────────────────────────────────────────────────── + +pub enum ViewportPaneMode { + /// Full model space — fills whatever bounds Iced assigns. + Model, + /// Paper-space entities only (title blocks, frames, borders) using the + /// paper-space camera. No viewport content projection. + PaperSheet, + /// Model-space content seen through a specific paper-space Viewport entity. + Paper { handle: Handle }, +} + +// ── Widget struct ───────────────────────────────────────────────────────── + +pub struct ViewportPane<'a> { + pub scene: &'a Scene, + pub mode: ViewportPaneMode, +} + +impl<'a> ViewportPane<'a> { + pub fn model(scene: &'a Scene) -> Self { + Self { scene, mode: ViewportPaneMode::Model } + } + + /// Paper-sheet layer: paper-space entities rendered with the paper camera. + pub fn paper_sheet(scene: &'a Scene) -> Self { + Self { scene, mode: ViewportPaneMode::PaperSheet } + } + + /// One paper-space viewport: model content rendered through its own camera. + pub fn paper(scene: &'a Scene, handle: Handle) -> Self { + Self { scene, mode: ViewportPaneMode::Paper { handle } } + } +} + +// ── shader::Program impl ────────────────────────────────────────────────── + +impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for ViewportPane<'a> { + type State = CameraState; + type Primitive = Primitive; + + fn draw( + &self, + state: &Self::State, + _cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + match &self.mode { + ViewportPaneMode::Model => { + self.scene.build_primitive(state.hover_region, bounds) + } + ViewportPaneMode::PaperSheet => { + self.scene.build_paper_sheet_primitive(state.hover_region, bounds) + } + ViewportPaneMode::Paper { handle } => { + self.scene.build_viewport_primitive(*handle, state.hover_region, bounds) + } + } + } + + fn update( + &self, + state: &mut Self::State, + event: &Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + // ViewCube hover only makes sense in the full model-space view. + if matches!(self.mode, ViewportPaneMode::Model | ViewportPaneMode::PaperSheet) { + 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 { + if matches!(self.mode, ViewportPaneMode::Model | ViewportPaneMode::PaperSheet) { + self.scene.viewcube_mouse_interaction(state) + } else { + mouse::Interaction::default() + } + } +}