feat(paper): render the whole sheet via the shader, drop PaperCanvas
Paper space now renders entirely through the unified GPU shader, exactly like model space — there is no separate 2-D canvas. The full-canvas top-locked "sheet" viewport draws the layout's entities, hatches, wipeouts and images; the desk is the container background and the white printable area is a synthetic solid hatch. - The white sheet fill uses draw_depth -2.0 (entity fills/wires are in the signed (-1, 1) range) so it is always strictly behind every object, with a negligible z offset (BIAS 0.001) — no far-plane clipping. - Delete src/scene/paper_canvas.rs (the PaperCanvas widget) and its dead helpers (paper_canvas_wires / paper_sheet_wires / paper_canvas_cache). paper_canvas_hatches / _wipeouts remain — still used by paper hit-test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0ef99ff2fc
commit
206ad1a4aa
4 changed files with 52 additions and 247 deletions
|
|
@ -4,12 +4,11 @@ use super::history::history_dropdown_labels;
|
||||||
use super::document::{DynComponent, DynFieldEntry};
|
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::viewport_pane::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::{
|
||||||
button, canvas, column, container, mouse_area, pick_list, row, shader, stack, text, text_input,
|
button, column, container, mouse_area, pick_list, row, shader, stack, text, text_input,
|
||||||
Row, Space,
|
Row, Space,
|
||||||
};
|
};
|
||||||
use iced::window;
|
use iced::window;
|
||||||
|
|
@ -470,15 +469,19 @@ impl OpenCADStudio {
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
} else if is_paper {
|
} else if is_paper {
|
||||||
// Paper layout: 2-D sheet + paper entities + viewport borders
|
// Paper layout: the GPU shader renders everything — the desk is the
|
||||||
// underneath, unified GPU shader on top (only paints inside
|
// container background, the white sheet + paper entities + borders
|
||||||
// each content viewport's scissor rect so the sheet shows
|
// come from the full-canvas top-locked "sheet" viewport, and the
|
||||||
// through outside them).
|
// floating content viewports overlay it (same path as model space).
|
||||||
let paper_sheet: Element<'_, Message> =
|
const DESK: Color = Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 };
|
||||||
canvas(PaperCanvas::new(&tab.scene)).width(Fill).height(Fill).into();
|
|
||||||
stack![
|
stack![
|
||||||
paper_sheet,
|
container(viewport_3d)
|
||||||
container(viewport_3d).width(Fill).height(Fill),
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(DESK)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill),
|
||||||
selection_overlay,
|
selection_overlay,
|
||||||
viewport_mouse,
|
viewport_mouse,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ pub mod image_model;
|
||||||
pub mod mesh_model;
|
pub mod mesh_model;
|
||||||
pub mod model_solid;
|
pub mod model_solid;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
pub mod paper_canvas;
|
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod properties;
|
pub mod properties;
|
||||||
pub mod quadtree;
|
pub mod quadtree;
|
||||||
|
|
@ -609,10 +608,6 @@ pub struct Scene {
|
||||||
/// Stores projected + clipped wires in paper-space coordinates.
|
/// Stores projected + clipped wires in paper-space coordinates.
|
||||||
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
||||||
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
|
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
|
||||||
/// Full paper-canvas wire list (sheet + inactive-viewport projections + interim/preview).
|
|
||||||
/// Keyed by geometry_epoch — valid for all navigation frames (pan/zoom do not bump epoch).
|
|
||||||
/// Returns Arc so paper_canvas_wires() is O(1) on cache hits.
|
|
||||||
paper_canvas_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>,
|
|
||||||
/// Active layout name — "Model" or a paper space layout name.
|
/// Active layout name — "Model" or a paper space layout name.
|
||||||
pub current_layout: String,
|
pub current_layout: String,
|
||||||
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
||||||
|
|
@ -709,7 +704,6 @@ impl Scene {
|
||||||
viewport_wire_cache: RefCell::new(HashMap::new()),
|
viewport_wire_cache: RefCell::new(HashMap::new()),
|
||||||
paper_sheet_cache: RefCell::new(None),
|
paper_sheet_cache: RefCell::new(None),
|
||||||
paper_projected_cache: RefCell::new(HashMap::new()),
|
paper_projected_cache: RefCell::new(HashMap::new()),
|
||||||
paper_canvas_cache: RefCell::new(None),
|
|
||||||
current_layout: "Model".to_string(),
|
current_layout: "Model".to_string(),
|
||||||
hatches: HashMap::new(),
|
hatches: HashMap::new(),
|
||||||
meshes: HashMap::new(),
|
meshes: HashMap::new(),
|
||||||
|
|
@ -1137,6 +1131,30 @@ impl Scene {
|
||||||
|
|
||||||
/// Returns `(min, max)` paper-space limits for the current layout, or `None`
|
/// Returns `(min, max)` paper-space limits for the current layout, or `None`
|
||||||
/// when in Model space. Falls back to A4 landscape if nothing reliable is found.
|
/// when in Model space. Falls back to A4 landscape if nothing reliable is found.
|
||||||
|
/// A solid white fill covering the paper sheet's printable area, rendered
|
||||||
|
/// by the GPU hatch pipeline behind the paper entities. Replaces the 2-D
|
||||||
|
/// white-rectangle the old PaperCanvas drew. `None` in model space or when
|
||||||
|
/// the layout has no limits.
|
||||||
|
pub(super) fn paper_sheet_fill(&self) -> Option<HatchModel> {
|
||||||
|
let ((x0, y0), (x1, y1)) = self.paper_limits()?;
|
||||||
|
let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
||||||
|
Some(HatchModel {
|
||||||
|
world_origin: [0.0, 0.0],
|
||||||
|
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
|
||||||
|
pattern: crate::scene::hatch_model::HatchPattern::Solid,
|
||||||
|
name: "SOLID".to_string(),
|
||||||
|
color: self.paper_bg_color,
|
||||||
|
angle_offset: 0.0,
|
||||||
|
scale: 1.0,
|
||||||
|
vp_scissor: None,
|
||||||
|
// Draw-order bias is signed: entity fills/wires land in (-1, 1)
|
||||||
|
// (0 = neutral). A value below -1 forces the sheet strictly behind
|
||||||
|
// EVERY object, in every case, with a tiny z offset (BIAS = 0.001,
|
||||||
|
// so no far-plane clipping). The sheet is the canvas, never on top.
|
||||||
|
draw_depth: -2.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paper_limits(&self) -> Option<((f64, f64), (f64, f64))> {
|
pub fn paper_limits(&self) -> Option<((f64, f64), (f64, f64))> {
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -5820,38 +5838,7 @@ impl Scene {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ViewportPane helpers ──────────────────────────────────────────────
|
// ── Paper-space 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.
|
|
||||||
/// All wires needed to render the paper-space canvas (2D widget path).
|
|
||||||
/// Includes paper entities, paper boundary, inactive viewport projections
|
|
||||||
/// (excluding the active MSPACE viewport), plus interim/preview wires.
|
|
||||||
#[allow(dead_code)] // superseded by the GPU sheet viewport; removed in Stage 2.
|
|
||||||
pub fn paper_canvas_wires(&self) -> Arc<Vec<WireModel>> {
|
|
||||||
{
|
|
||||||
let cache = self.paper_canvas_cache.borrow();
|
|
||||||
if let Some((cached_epoch, ref arc)) = *cache {
|
|
||||||
if cached_epoch == self.geometry_epoch {
|
|
||||||
return Arc::clone(arc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The unified GPU shader draws every content viewport (active and
|
|
||||||
// inactive) directly through its own camera + scissor, so the
|
|
||||||
// paper canvas no longer needs to CPU-project model content onto
|
|
||||||
// the sheet. `paper_sheet_wires()` keeps the title-block /
|
|
||||||
// annotation / viewport-border 2-D pass that GPU does not handle.
|
|
||||||
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());
|
|
||||||
let arc = Arc::new(wires);
|
|
||||||
*self.paper_canvas_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
|
||||||
arc
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hatch fills for the paper-space 2-D canvas. The GPU-rendered
|
/// Hatch fills for the paper-space 2-D canvas. The GPU-rendered
|
||||||
/// content viewports already draw model-block hatches inside their
|
/// content viewports already draw model-block hatches inside their
|
||||||
|
|
@ -5952,11 +5939,6 @@ impl Scene {
|
||||||
Arc::new(models)
|
Arc::new(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)] // superseded by the GPU sheet viewport; removed in Stage 2.
|
|
||||||
pub(super) fn paper_sheet_wires(&self) -> Vec<WireModel> {
|
|
||||||
(*self.paper_sheet_wires_arc()).clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a Camera oriented and scaled to match a paper-space Viewport entity.
|
/// Build a Camera oriented and scaled to match a paper-space Viewport entity.
|
||||||
/// Used by `ViewportPane::Paper` to render model-space content through the
|
/// Used by `ViewportPane::Paper` to render model-space content through the
|
||||||
/// viewport's own view direction and scale.
|
/// viewport's own view direction and scale.
|
||||||
|
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
//! 2-D Iced canvas widget for the paper-space view.
|
|
||||||
//!
|
|
||||||
//! Replaces the 3-D shader widget used for the paper sheet so that paper-space
|
|
||||||
//! entities (title blocks, annotations, viewport borders) can be interacted
|
|
||||||
//! with directly — no need to enter MSPACE just to click on them.
|
|
||||||
//!
|
|
||||||
//! A single unified `ViewportPane` shader widget is stacked on top of this
|
|
||||||
//! canvas to render every content viewport's 3-D model content into its own
|
|
||||||
//! scissor rectangle — outside those rectangles the shader does not paint,
|
|
||||||
//! so the paper sheet drawn here shows through.
|
|
||||||
|
|
||||||
use iced::widget::canvas;
|
|
||||||
use iced::{mouse, Color, Point, Rectangle};
|
|
||||||
|
|
||||||
use super::hatch_model::{HatchModel, HatchPattern};
|
|
||||||
use super::Scene;
|
|
||||||
use crate::app::Message;
|
|
||||||
|
|
||||||
// ── PaperCanvas ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct PaperCanvas<'a> {
|
|
||||||
pub scene: &'a Scene,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PaperCanvas<'a> {
|
|
||||||
pub fn new(scene: &'a Scene) -> Self {
|
|
||||||
Self { scene }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── canvas::Program impl ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
impl<'a> canvas::Program<Message> for PaperCanvas<'a> {
|
|
||||||
type State = ();
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
_state: &(),
|
|
||||||
renderer: &iced::Renderer,
|
|
||||||
_theme: &iced::Theme,
|
|
||||||
bounds: Rectangle,
|
|
||||||
_cursor: mouse::Cursor,
|
|
||||||
) -> Vec<canvas::Geometry> {
|
|
||||||
// Update vp_size so viewport_screen_rect() knows the canvas dimensions
|
|
||||||
// and can position the MSPACE shader overlay correctly.
|
|
||||||
self.scene.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
|
|
||||||
|
|
||||||
let cam = self.scene.camera.borrow();
|
|
||||||
let aspect = if bounds.height > 0.0 {
|
|
||||||
bounds.width / bounds.height
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let half_h = cam.ortho_size();
|
|
||||||
let half_w = half_h * aspect;
|
|
||||||
let tx = cam.target.x;
|
|
||||||
let ty = cam.target.y;
|
|
||||||
drop(cam);
|
|
||||||
|
|
||||||
// Closure: paper-space world coords → canvas pixel coords.
|
|
||||||
let to_px = move |wx: f32, wy: f32| Point {
|
|
||||||
x: (wx - tx + half_w) / (2.0 * half_w) * bounds.width,
|
|
||||||
y: (ty + half_h - wy) / (2.0 * half_h) * bounds.height,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
|
||||||
|
|
||||||
// ── Desk background (area outside the paper sheet) ────────────────────
|
|
||||||
const DESK: Color = Color {
|
|
||||||
r: 0.22,
|
|
||||||
g: 0.24,
|
|
||||||
b: 0.28,
|
|
||||||
a: 1.0,
|
|
||||||
};
|
|
||||||
frame.fill_rectangle(Point::ORIGIN, bounds.size(), DESK);
|
|
||||||
|
|
||||||
// ── White paper area — use layout paper limits (actual paper size).
|
|
||||||
if let Some(((px0, py0), (px1, py1))) = self
|
|
||||||
.scene
|
|
||||||
.paper_limits()
|
|
||||||
.map(|((x0, y0), (x1, y1))| ((x0 as f32, y0 as f32), (x1 as f32, y1 as f32)))
|
|
||||||
{
|
|
||||||
let tl = to_px(px0, py1);
|
|
||||||
let br = to_px(px1, py0);
|
|
||||||
let pw = br.x - tl.x;
|
|
||||||
let ph = br.y - tl.y;
|
|
||||||
if pw > 0.0 && ph > 0.0 {
|
|
||||||
let [r, g, b, a] = self.scene.paper_bg_color;
|
|
||||||
frame.fill_rectangle(tl, iced::Size::new(pw, ph), Color { r, g, b, a });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Wipeout fills (rendered before wires, cover background) ──────────
|
|
||||||
for hatch in self.scene.paper_canvas_wipeouts().iter() {
|
|
||||||
draw_hatch(&mut frame, hatch, &to_px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hatch fills ───────────────────────────────────────────────────────
|
|
||||||
for hatch in self.scene.paper_canvas_hatches().iter() {
|
|
||||||
draw_hatch(&mut frame, hatch, &to_px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entity lines, viewport borders and interim/preview wires are now
|
|
||||||
// rendered by the GPU shader's full-canvas paper "sheet" viewport, so
|
|
||||||
// the 2-D canvas only paints the desk + sheet + fills beneath it.
|
|
||||||
vec![frame.into_geometry()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Draw one HatchModel (solid, gradient, or pattern) onto `frame`.
|
|
||||||
/// Patterns and gradients are approximated as solid fills in 2-D canvas mode.
|
|
||||||
fn draw_hatch(frame: &mut canvas::Frame, hatch: &HatchModel, to_px: &impl Fn(f32, f32) -> Point) {
|
|
||||||
if hatch.boundary.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [r, g, b, a] = hatch.color;
|
|
||||||
let color = Color { r, g, b, a };
|
|
||||||
|
|
||||||
// Reconstruct offset-rel WCS from stored small offsets + f64 anchor.
|
|
||||||
let ox = hatch.world_origin[0] as f32;
|
|
||||||
let oy = hatch.world_origin[1] as f32;
|
|
||||||
// `boundary` may carry multiple disconnected sub-paths separated by
|
|
||||||
// NaN-NaN sentinels (multi-region hatches with islands / holes).
|
|
||||||
// Forwarding NaN to lyon's path builder panics
|
|
||||||
// (`IncorrectActiveEdgeOrder`) — start a fresh sub-path on each
|
|
||||||
// sentinel so each region is closed and filled independently.
|
|
||||||
let path = canvas::Path::new(|builder| {
|
|
||||||
let mut active = false;
|
|
||||||
for &[x, y] in hatch.boundary.iter() {
|
|
||||||
if x.is_nan() || y.is_nan() {
|
|
||||||
if active {
|
|
||||||
builder.close();
|
|
||||||
active = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let p = to_px(x + ox, y + oy);
|
|
||||||
if !active {
|
|
||||||
builder.move_to(p);
|
|
||||||
active = true;
|
|
||||||
} else {
|
|
||||||
builder.line_to(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if active {
|
|
||||||
builder.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match &hatch.pattern {
|
|
||||||
HatchPattern::Solid => {
|
|
||||||
frame.fill(&path, color);
|
|
||||||
}
|
|
||||||
HatchPattern::Pattern(_) => {
|
|
||||||
// Rasterise the PAT line families clipped to the boundary so
|
|
||||||
// the paper canvas shows the actual hatch lines (ANSI31, etc.)
|
|
||||||
// instead of just the outline. Matches the GPU shader output
|
|
||||||
// for model-space hatches.
|
|
||||||
for [a, b] in hatch.pattern_segments() {
|
|
||||||
let seg = canvas::Path::new(|builder| {
|
|
||||||
builder.move_to(to_px(a[0], a[1]));
|
|
||||||
builder.line_to(to_px(b[0], b[1]));
|
|
||||||
});
|
|
||||||
frame.stroke(
|
|
||||||
&seg,
|
|
||||||
canvas::Stroke {
|
|
||||||
style: canvas::Style::Solid(color),
|
|
||||||
width: 1.0,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HatchPattern::Gradient { color2, .. } => {
|
|
||||||
// Gradient: average the two colours as a solid fill.
|
|
||||||
let avg = Color {
|
|
||||||
r: (color.r + color2[0]) * 0.5,
|
|
||||||
g: (color.g + color2[1]) * 0.5,
|
|
||||||
b: (color.b + color2[2]) * 0.5,
|
|
||||||
a: (color.a + color2[3]) * 0.5,
|
|
||||||
};
|
|
||||||
frame.fill(&path, avg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -653,14 +653,22 @@ impl Scene {
|
||||||
height: visible_h / canvas.1,
|
height: visible_h / canvas.1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stage 1: the paper sheet instance contributes only entity wires; its
|
// The paper sheet instance renders the same hatch / wipeout / image
|
||||||
// hatch / wipeout / image fills are still drawn by PaperCanvas beneath
|
// fills as everything else (wrong-camera fills fall outside its
|
||||||
// the shader. (Stage 2 moves those into the shader and drops PaperCanvas.)
|
// frustum and are culled), plus a synthetic white fill for the sheet's
|
||||||
let (hatches, wipeout_hatches, images) = if inst.paper_sheet {
|
// printable area — the GPU equivalent of the old PaperCanvas chrome.
|
||||||
(Arc::new(Vec::new()), Arc::new(Vec::new()), Arc::new(Vec::new()))
|
let hatches = if inst.paper_sheet {
|
||||||
|
let mut v: Vec<HatchModel> = Vec::new();
|
||||||
|
if let Some(sheet) = self.paper_sheet_fill() {
|
||||||
|
v.push(sheet);
|
||||||
|
}
|
||||||
|
v.extend(self.hatch_models_arc().iter().cloned());
|
||||||
|
Arc::new(v)
|
||||||
} else {
|
} else {
|
||||||
(self.hatch_models_arc(), self.wipeout_models_arc(), self.images_arc())
|
self.hatch_models_arc()
|
||||||
};
|
};
|
||||||
|
let wipeout_hatches = self.wipeout_models_arc();
|
||||||
|
let images = self.images_arc();
|
||||||
|
|
||||||
Some(ViewportData {
|
Some(ViewportData {
|
||||||
wires: all_wires,
|
wires: all_wires,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue