feat: introduce ViewportPane widget for unified model/paper rendering
- Remove Scene: shader::Program<Msg> 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 <noreply@anthropic.com>
This commit is contained in:
parent
de04b7e351
commit
da9fafc0f8
5 changed files with 594 additions and 81 deletions
143
VIEWPORT_WIDGET_PLAN.md
Normal file
143
VIEWPORT_WIDGET_PLAN.md
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<!--
|
||||||
|
CONTEXT PROMPT FOR AI ASSISTANT
|
||||||
|
=================================
|
||||||
|
You are working on H7CAD, a CAD application written in Rust using the Iced GUI framework and wgpu for GPU rendering. The project is at /home/hakanseven/Kodlama/H7CAD.
|
||||||
|
|
||||||
|
This document describes a planned refactoring. Steps 1–4 are DONE and committed. Steps 5–6 remain.
|
||||||
|
|
||||||
|
Key files to read before continuing:
|
||||||
|
- src/scene/viewport_pane.rs — NEW: ViewportPane widget (Model / PaperSheet / Paper modes)
|
||||||
|
- src/scene/render.rs — Scene render helpers (build_primitive, build_paper_sheet_primitive, build_viewport_primitive)
|
||||||
|
- src/scene/mod.rs — Scene struct; viewport_screen_rect(), camera_for_viewport(), model_wires_for_viewport(), paper_sheet_wires()
|
||||||
|
- src/app/view.rs — paper_canvas_view() function; view() uses ViewportPane::model + paper_canvas_view
|
||||||
|
- src/scene/camera.rs — Camera struct (arcball, orthographic/perspective)
|
||||||
|
- src/entities/viewport.rs — Viewport DXF entity: grips, properties, transform impls
|
||||||
|
- src/ui/statusbar.rs — layout tab bar (space_tab function, LayoutSwitch message)
|
||||||
|
|
||||||
|
Architecture summary (after Steps 1–4):
|
||||||
|
- Model tab: shader(ViewportPane::model(&tab.scene)).width(Fill).height(Fill)
|
||||||
|
- Paper tab: paper_canvas_view(tab) which builds a stack![] of:
|
||||||
|
layer 1: shader(ViewportPane::paper_sheet(scene)) — paper-space entities, paper camera
|
||||||
|
layer 2+: shader(ViewportPane::paper(scene, handle)) per Viewport entity,
|
||||||
|
positioned with Space offsets computed from viewport_screen_rect()
|
||||||
|
- Mouse events in paper space still route through the same viewport_mouse mouse_area overlay.
|
||||||
|
- ViewCube is shown in Model and PaperSheet modes; hidden in Paper mode.
|
||||||
|
|
||||||
|
What still needs to be done (Steps 5–6):
|
||||||
|
5. Mouse routing per-viewport in paper space (MSPACE double-click to enter, Escape to exit).
|
||||||
|
Currently the viewport_mouse mouse_area covers the full canvas; MSPACE detection already
|
||||||
|
works via Scene::active_viewport + viewport_at_paper_point(). Needs wiring to route
|
||||||
|
pan/zoom/select to the correct viewport widget.
|
||||||
|
6. Per-viewport layer freeze already works in build_viewport_primitive via model_wires_for_viewport().
|
||||||
|
Verify that the existing frozen_layers list on each Viewport entity is respected correctly.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Unified ViewportPane Widget Plan
|
||||||
|
|
||||||
|
## Current State (as of implementation start)
|
||||||
|
|
||||||
|
- `src/scene/render.rs`: `Scene` implements `shader::Program<Msg>` → 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<Msg>`. `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<iced::Rectangle>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -4,6 +4,7 @@ use super::history::history_dropdown_labels;
|
||||||
use super::helpers::grid_plane_from_camera;
|
use super::helpers::grid_plane_from_camera;
|
||||||
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
||||||
use crate::scene::grip::grips_to_screen;
|
use crate::scene::grip::grips_to_screen;
|
||||||
|
use crate::scene::viewport_pane::ViewportPane;
|
||||||
use crate::ui::overlay;
|
use crate::ui::overlay;
|
||||||
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row, Space};
|
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row, Space};
|
||||||
use iced::window;
|
use iced::window;
|
||||||
|
|
@ -103,7 +104,14 @@ impl H7CAD {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let tab = &self.tabs[i];
|
let tab = &self.tabs[i];
|
||||||
let is_paper = tab.scene.current_layout != "Model";
|
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 selection_overlay = {
|
||||||
let sel = tab.scene.selection.borrow().clone();
|
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 ───────────────────────────────────────────────────────
|
// ── 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> {
|
||||||
|
|
|
||||||
151
src/scene/mod.rs
151
src/scene/mod.rs
|
|
@ -18,6 +18,7 @@ pub mod solid3d_tess;
|
||||||
pub mod tessellate;
|
pub mod tessellate;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
pub mod truck_tess;
|
pub mod truck_tess;
|
||||||
|
pub mod viewport_pane;
|
||||||
pub mod wire_model;
|
pub mod wire_model;
|
||||||
|
|
||||||
use camera::Camera;
|
use camera::Camera;
|
||||||
|
|
@ -2095,6 +2096,156 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, _dt: Duration) {}
|
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<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) {
|
||||||
|
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<WireModel> {
|
||||||
|
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<camera::Camera> {
|
||||||
|
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<WireModel> {
|
||||||
|
use std::collections::HashSet as HSet;
|
||||||
|
|
||||||
|
let frozen: HSet<Handle> = 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 {
|
impl Default for Scene {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
|
|
||||||
use acadrust::tables::LineType;
|
use acadrust::tables::LineType;
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
use acadrust::EntityType;
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::Mat4;
|
use glam::Mat4;
|
||||||
use iced::mouse;
|
use iced::mouse;
|
||||||
use iced::widget::shader::{self, Viewport};
|
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::viewcube::{hover_id, VIEWCUBE_PX};
|
||||||
use super::pipeline::Pipeline;
|
use super::pipeline::Pipeline;
|
||||||
|
|
@ -40,84 +40,6 @@ pub struct Primitive {
|
||||||
pub(super) bg_color: [f32; 4],
|
pub(super) bg_color: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shader::Program impl ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
impl<Msg: std::fmt::Debug + Clone> shader::Program<Msg> 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<iced::widget::Action<Msg>> {
|
|
||||||
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 ────────────────────────────────────────────────
|
// ── shader::Primitive impl ────────────────────────────────────────────────
|
||||||
|
|
||||||
impl shader::Primitive for Primitive {
|
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<usize>,
|
||||||
|
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<usize>,
|
||||||
|
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<usize>,
|
||||||
|
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 ───────────────────────────────────────────────
|
// ── Linetype pattern helper ───────────────────────────────────────────────
|
||||||
|
|
||||||
pub(super) fn resolve_pattern(
|
pub(super) fn resolve_pattern(
|
||||||
|
|
|
||||||
94
src/scene/viewport_pane.rs
Normal file
94
src/scene/viewport_pane.rs
Normal file
|
|
@ -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<Msg> 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<iced::widget::Action<Msg>> {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue