forked from 1b-agent/opencadstudio-upstream
perf(scene): extend viewport scissor to hatch / image / wipeout passes (Phase 1.4)
Wire pass already uses `WireModel.vp_scissor` to clip viewport-projected geometry to its paper-space viewport rect; hatch / image / wipeout passes drew across the whole canvas. When a future paper-space path projects fills/images into a viewport, they would bleed past the viewport frame. Add `vp_scissor: Option<[f32; 4]>` to HatchModel and ImageModel (mirroring WireModel), forward it through HatchGpu / ImageGpu, and add per-pass `compute_*_scissors` / `set_scissor_rect` machinery. Factored the wire-side scissor-projection math into a shared `project_scissor` helper. Field defaults to None at every constructor site — no producer sets it yet, so this is plumbing for the paper-viewport fill projection that Phase 1.4 unblocks without changing current visual output. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
9a4531bd8e
commit
fbbac423d4
8 files changed
+145
-24
No files matched your search
@@ -119,6 +119,7 @@ impl HatchCommand {
|
||||
color: [0.75, 0.75, 0.75, 0.85],
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
vp_scissor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,6 +255,7 @@ impl GradientCommand {
|
||||
color: [0.30, 0.60, 0.95, 0.80],
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
vp_scissor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,6 +404,7 @@ impl CadCommand for BoundaryCommand {
|
||||
color: [0.45, 0.45, 0.45, 0.60],
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
vp_scissor: None,
|
||||
};
|
||||
return CmdResult::CommitHatch(model);
|
||||
}
|
||||
|
||||
@@ -58,4 +58,10 @@ pub struct HatchModel {
|
||||
pub angle_offset: f32,
|
||||
/// Pattern scale multiplier (from DXF `pattern_scale`).
|
||||
pub scale: f32,
|
||||
/// Optional world-space XY rect [x0, y0, x1, y1] for paper-space
|
||||
/// viewport clipping. When `Some`, the pipeline translates it into
|
||||
/// a per-frame pixel scissor and clips this hatch's draw call to it,
|
||||
/// preventing viewport-projected content from bleeding past the
|
||||
/// viewport frame. Mirrors `WireModel.vp_scissor`.
|
||||
pub vp_scissor: Option<[f32; 4]>,
|
||||
}
|
||||
@@ -23,6 +23,10 @@ pub struct ImageModel {
|
||||
/// [2] origin + U*W + V*H (top-right)
|
||||
/// [3] origin + V*H (top-left)
|
||||
pub corners: [[f32; 3]; 4],
|
||||
/// Optional world-space XY rect [x0, y0, x1, y1] for paper-space
|
||||
/// viewport clipping. Mirrors `WireModel.vp_scissor` /
|
||||
/// `HatchModel.vp_scissor`.
|
||||
pub vp_scissor: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl ImageModel {
|
||||
@@ -56,6 +60,7 @@ impl ImageModel {
|
||||
height,
|
||||
opacity,
|
||||
corners,
|
||||
vp_scissor: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2315,6 +2315,7 @@ impl Scene {
|
||||
color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
vp_scissor: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2367,6 +2368,7 @@ impl Scene {
|
||||
color: fill_color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
vp_scissor: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2658,6 +2660,7 @@ impl Scene {
|
||||
color,
|
||||
angle_offset: dxf.pattern_angle as f32,
|
||||
scale: dxf.pattern_scale as f32,
|
||||
vp_scissor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2806,6 +2809,7 @@ impl Scene {
|
||||
color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
vp_scissor: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ pub struct FamilyBatchData {
|
||||
pub struct HatchGpu {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
/// Mirrors `HatchModel.vp_scissor`. Forwarded to the pipeline so each
|
||||
/// frame's `compute_hatch_scissors` can translate the world rect into
|
||||
/// a pixel scissor.
|
||||
pub vp_scissor: Option<[f32; 4]>,
|
||||
_uniform_buf: wgpu::Buffer,
|
||||
_boundary_buf: wgpu::Buffer,
|
||||
_family_buf: wgpu::Buffer,
|
||||
@@ -268,6 +272,7 @@ impl HatchGpu {
|
||||
Self {
|
||||
vertex_buffer,
|
||||
bind_group,
|
||||
vp_scissor: model.vp_scissor,
|
||||
_uniform_buf,
|
||||
_boundary_buf,
|
||||
_family_buf,
|
||||
|
||||
@@ -53,6 +53,8 @@ struct ImageParams {
|
||||
pub struct ImageGpu {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
/// Mirrors `ImageModel.vp_scissor` — see `HatchGpu.vp_scissor`.
|
||||
pub vp_scissor: Option<[f32; 4]>,
|
||||
_texture: wgpu::Texture,
|
||||
_sampler: wgpu::Sampler,
|
||||
_params_buf: wgpu::Buffer,
|
||||
@@ -183,6 +185,7 @@ impl ImageGpu {
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
bind_group,
|
||||
vp_scissor: model.vp_scissor,
|
||||
_texture: texture,
|
||||
_sampler,
|
||||
_params_buf,
|
||||
|
||||
+116
-24
@@ -57,9 +57,15 @@ pub struct Pipeline {
|
||||
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
||||
gpu_selected_wires: Vec<WireGpu>,
|
||||
gpu_hatches: Vec<HatchGpu>,
|
||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped hatches. Recomputed each frame.
|
||||
hatch_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||
/// Wipeout fills — rendered after wires in a separate pass.
|
||||
gpu_wipeouts: Vec<HatchGpu>,
|
||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wipeouts. Recomputed each frame.
|
||||
wipeout_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||
gpu_images: Vec<ImageGpu>,
|
||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped images. Recomputed each frame.
|
||||
image_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||
gpu_meshes: Vec<MeshGpu>,
|
||||
/// Batched 3DFACE fill (all faces in one buffer) and edges (merged wire).
|
||||
gpu_face3d_fill: Option<Face3DGpu>,
|
||||
@@ -590,8 +596,11 @@ impl Pipeline {
|
||||
wire_pixel_scissors: vec![],
|
||||
gpu_selected_wires: vec![],
|
||||
gpu_hatches: vec![],
|
||||
hatch_pixel_scissors: vec![],
|
||||
gpu_wipeouts: vec![],
|
||||
wipeout_pixel_scissors: vec![],
|
||||
gpu_images: vec![],
|
||||
image_pixel_scissors: vec![],
|
||||
gpu_meshes: vec![],
|
||||
gpu_face3d_fill: None,
|
||||
gpu_face3d_edges: vec![],
|
||||
@@ -615,30 +624,37 @@ impl Pipeline {
|
||||
/// Recompute pixel scissor rects for viewport-clipped wires from the current view_proj.
|
||||
/// Called every frame from prepare() because scissor pixels shift with pan/zoom.
|
||||
pub fn compute_wire_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||
let w = clip_w as f32;
|
||||
let h = clip_h as f32;
|
||||
self.wire_pixel_scissors = self
|
||||
.gpu_wires
|
||||
.iter()
|
||||
.map(|wire| {
|
||||
let [x0, y0, x1, y1] = wire.vp_scissor?;
|
||||
let corners = [
|
||||
view_proj.project_point3(glam::Vec3::new(x0, y0, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x1, y0, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x0, y1, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x1, y1, 0.0)),
|
||||
];
|
||||
let px: Vec<f32> = corners.iter().map(|c| (c.x + 1.0) * 0.5 * w).collect();
|
||||
let py: Vec<f32> = corners.iter().map(|c| (1.0 - c.y) * 0.5 * h).collect();
|
||||
let sx0 = px.iter().cloned().fold(f32::INFINITY, f32::min).max(0.0) as u32;
|
||||
let sy0 = py.iter().cloned().fold(f32::INFINITY, f32::min).max(0.0) as u32;
|
||||
let sx1 = (px.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as u32).min(clip_w);
|
||||
let sy1 = (py.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as u32).min(clip_h);
|
||||
if sx1 <= sx0 || sy1 <= sy0 {
|
||||
return None;
|
||||
}
|
||||
Some([sx0, sy0, sx1 - sx0, sy1 - sy0])
|
||||
})
|
||||
.map(|w| project_scissor(w.vp_scissor, view_proj, clip_w, clip_h))
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Recompute pixel scissor rects for viewport-clipped hatches.
|
||||
pub fn compute_hatch_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||
self.hatch_pixel_scissors = self
|
||||
.gpu_hatches
|
||||
.iter()
|
||||
.map(|h| project_scissor(h.vp_scissor, view_proj, clip_w, clip_h))
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Recompute pixel scissor rects for viewport-clipped wipeouts.
|
||||
pub fn compute_wipeout_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||
self.wipeout_pixel_scissors = self
|
||||
.gpu_wipeouts
|
||||
.iter()
|
||||
.map(|h| project_scissor(h.vp_scissor, view_proj, clip_w, clip_h))
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Recompute pixel scissor rects for viewport-clipped raster images.
|
||||
pub fn compute_image_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||
self.image_pixel_scissors = self
|
||||
.gpu_images
|
||||
.iter()
|
||||
.map(|i| project_scissor(i.vp_scissor, view_proj, clip_w, clip_h))
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -749,11 +765,26 @@ impl Pipeline {
|
||||
if !self.gpu_hatches.is_empty() {
|
||||
pass.set_pipeline(&self.hatch_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for hatch in &self.gpu_hatches {
|
||||
let mut scissor_active = false;
|
||||
for (i, hatch) in self.gpu_hatches.iter().enumerate() {
|
||||
match self.hatch_pixel_scissors.get(i) {
|
||||
Some(Some([x, y, w, h])) => {
|
||||
pass.set_scissor_rect(*x, *y, *w, *h);
|
||||
scissor_active = true;
|
||||
}
|
||||
_ if scissor_active => {
|
||||
pass.set_scissor_rect(0, 0, vp.width, vp.height);
|
||||
scissor_active = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
if scissor_active {
|
||||
pass.set_scissor_rect(0, 0, vp.width, vp.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,11 +815,26 @@ impl Pipeline {
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.image_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for img in &self.gpu_images {
|
||||
let mut scissor_active = false;
|
||||
for (i, img) in self.gpu_images.iter().enumerate() {
|
||||
match self.image_pixel_scissors.get(i) {
|
||||
Some(Some([x, y, w, h])) => {
|
||||
pass.set_scissor_rect(*x, *y, *w, *h);
|
||||
scissor_active = true;
|
||||
}
|
||||
_ if scissor_active => {
|
||||
pass.set_scissor_rect(0, 0, vp.width, vp.height);
|
||||
scissor_active = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pass.set_bind_group(1, &img.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, img.vertex_buffer.slice(..));
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
if scissor_active {
|
||||
pass.set_scissor_rect(0, 0, vp.width, vp.height);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 4: solid meshes ──────────────────────────────────────────
|
||||
@@ -973,11 +1019,26 @@ impl Pipeline {
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.hatch_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for wipeout in &self.gpu_wipeouts {
|
||||
let mut scissor_active = false;
|
||||
for (i, wipeout) in self.gpu_wipeouts.iter().enumerate() {
|
||||
match self.wipeout_pixel_scissors.get(i) {
|
||||
Some(Some([x, y, w, h])) => {
|
||||
pass.set_scissor_rect(*x, *y, *w, *h);
|
||||
scissor_active = true;
|
||||
}
|
||||
_ if scissor_active => {
|
||||
pass.set_scissor_rect(0, 0, vp.width, vp.height);
|
||||
scissor_active = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pass.set_bind_group(1, &wipeout.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, wipeout.vertex_buffer.slice(..));
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
if scissor_active {
|
||||
pass.set_scissor_rect(0, 0, vp.width, vp.height);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 7: selected wire overlay pass ───────────────────────────
|
||||
@@ -1100,6 +1161,37 @@ impl Pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
/// Project a world-space XY scissor rect through `view_proj` into the four
|
||||
/// pixel-space corners and return the smallest aligned-rect that bounds them,
|
||||
/// clamped to the clip viewport. Returns `None` when the rect is missing or
|
||||
/// the projection collapses (off-screen, behind camera).
|
||||
fn project_scissor(
|
||||
rect: Option<[f32; 4]>,
|
||||
view_proj: glam::Mat4,
|
||||
clip_w: u32,
|
||||
clip_h: u32,
|
||||
) -> Option<[u32; 4]> {
|
||||
let [x0, y0, x1, y1] = rect?;
|
||||
let w = clip_w as f32;
|
||||
let h = clip_h as f32;
|
||||
let corners = [
|
||||
view_proj.project_point3(glam::Vec3::new(x0, y0, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x1, y0, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x0, y1, 0.0)),
|
||||
view_proj.project_point3(glam::Vec3::new(x1, y1, 0.0)),
|
||||
];
|
||||
let px: Vec<f32> = corners.iter().map(|c| (c.x + 1.0) * 0.5 * w).collect();
|
||||
let py: Vec<f32> = corners.iter().map(|c| (1.0 - c.y) * 0.5 * h).collect();
|
||||
let sx0 = px.iter().cloned().fold(f32::INFINITY, f32::min).max(0.0) as u32;
|
||||
let sy0 = py.iter().cloned().fold(f32::INFINITY, f32::min).max(0.0) as u32;
|
||||
let sx1 = (px.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as u32).min(clip_w);
|
||||
let sy1 = (py.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as u32).min(clip_h);
|
||||
if sx1 <= sx0 || sy1 <= sy0 {
|
||||
return None;
|
||||
}
|
||||
Some([sx0, sy0, sx1 - sx0, sy1 - sy0])
|
||||
}
|
||||
|
||||
fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("viewer.depth_texture"),
|
||||
|
||||
@@ -144,6 +144,9 @@ impl shader::Primitive for Primitive {
|
||||
pipeline.cached_epoch = cur_key;
|
||||
}
|
||||
pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||
pipeline.compute_hatch_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||
pipeline.compute_wipeout_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||
pipeline.compute_image_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||
if self.show_viewcube {
|
||||
pipeline.viewcube.upload(
|
||||
queue,
|
||||
|
||||
Reference in new issue
Block a user