feat: GPU scissor rect for paper-space viewport wires

Viewport-projected wires now clip at pixel level to their viewport boundary.

- WireModel.vp_scissor: Option<[f32; 4]> — paper-space bbox set on projection
- WireGpu.vp_scissor carries it through to the draw loop
- Pipeline.compute_wire_scissors() converts paper→NDC→pixel each frame
- render() calls set_scissor_rect() per wire, reset after viewport group
- INTEGRATION_GAPS.md: Viewport ⚠️ → ✅, all 51 gaps resolved

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-05-06 07:12:05 +03:00
1 parent 27aca41dd1
commit 899b3f4796
38 files changed
+112 -13

No files matched your search

+4 -8
View File
@@ -55,7 +55,7 @@ Legend: ✅ Done · ⚠️ Partial · ❌ Not done
| Status | Entity | What works | What's missing |
|---|---|---|---|
| ⚠️ | **Viewport** (paper space) | Frame (active=gold, inactive=cyan), CPU projection + Cohen-Sutherland clipping, perspective, layer freeze per-viewport, MSPACE switching, pan/zoom/orbit | GPU scissor rect for pixel-level boundary clipping; stencil masking for overlapping viewports |
| ✅ | **Viewport** (paper space) | Frame (active=gold, inactive=cyan), CPU projection + Cohen-Sutherland clipping, perspective, layer freeze per-viewport, MSPACE switching, pan/zoom/orbit, GPU scissor rect per viewport wire | Stencil masking for overlapping viewports (rare edge case) |
---
@@ -207,13 +207,9 @@ These are fixed in our post-load `fix_dxf_dimension_rotations()` in `src/io/mod.
| Status | Count |
|---|---|
| ✅ Done | 50 |
| ⚠️ Partial | 1 |
| ✅ Done | 51 |
| ⚠️ Partial | 0 |
| ❌ Not done | 0 |
| **Total** | **51** |
### Remaining gaps by priority
**Medium:** Viewport GPU scissor rect (pixel-level boundary clipping for overlapping viewports)
No remaining low-priority gaps.
All gaps resolved.
+1
View File
@@ -1367,6 +1367,7 @@ impl H7CAD {
key_vertices: vec![],
aabb: crate::scene::WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
};
previews.push(guide);
}
+2
View File
@@ -103,6 +103,7 @@ impl CadCommand for AlignedDimensionCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
@@ -138,5 +139,6 @@ fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -126,6 +126,7 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -102,5 +102,6 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -144,6 +144,7 @@ impl CadCommand for DimBaselineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -141,6 +141,7 @@ impl CadCommand for DimContinueCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -95,6 +95,7 @@ impl CadCommand for DimJogLineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -103,6 +103,7 @@ impl CadCommand for DimTeditCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
+1
View File
@@ -203,6 +203,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -114,6 +114,7 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -156,6 +156,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -367,6 +367,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -100,6 +100,7 @@ impl CadCommand for QdimCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -113,5 +113,6 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -146,6 +146,7 @@ impl CadCommand for TableCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
} else {
None
+1
View File
@@ -99,6 +99,7 @@ impl CadCommand for ToleranceCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -135,6 +135,7 @@ impl CadCommand for AttdefCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -528,5 +528,6 @@ fn line_wire(from: Vec3, to: Vec3) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
+1
View File
@@ -89,6 +89,7 @@ impl CadCommand for LineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -114,6 +114,7 @@ impl CadCommand for MlineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -113,6 +113,7 @@ impl CadCommand for ImageCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+2
View File
@@ -87,6 +87,7 @@ impl CadCommand for RayCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
@@ -166,6 +167,7 @@ impl CadCommand for XLineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -84,6 +84,7 @@ impl CadCommand for RevCloudCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+2
View File
@@ -120,6 +120,7 @@ impl CadCommand for WipeoutCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
WipeoutMode::Polygonal => {
@@ -142,6 +143,7 @@ impl CadCommand for WipeoutCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -77,6 +77,7 @@ impl CadCommand for AreaCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -72,6 +72,7 @@ impl CadCommand for DistCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -135,6 +135,7 @@ impl CadCommand for StretchCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
_ => None,
+1
View File
@@ -99,6 +99,7 @@ impl CadCommand for MviewCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -64,6 +64,7 @@ impl CadCommand for PlotWindowCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -70,6 +70,7 @@ impl CadCommand for ZoomWindowCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
}
}
+1
View File
@@ -203,6 +203,7 @@ pub fn apply_along(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
})
.collect()
}
+2
View File
@@ -976,6 +976,7 @@ impl Scene {
out.points = clipped;
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
out.line_weight_px = wire.line_weight_px;
out.vp_scissor = Some([vp_x0, vp_y0, vp_x1, vp_y1]);
projected.push(out);
}
@@ -3212,6 +3213,7 @@ fn tessellate_entity(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
});
return wires;
}
+47 -4
View File
@@ -52,6 +52,8 @@ pub struct Pipeline {
/// Cached texture format (needed to recreate MSAA / depth textures on resize).
surface_format: wgpu::TextureFormat,
gpu_wires: Vec<WireGpu>,
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame.
wire_pixel_scissors: Vec<Option<[u32; 4]>>,
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
gpu_selected_wires: Vec<WireGpu>,
gpu_hatches: Vec<HatchGpu>,
@@ -584,6 +586,7 @@ impl Pipeline {
blit_bind_group,
surface_format: format,
gpu_wires: vec![],
wire_pixel_scissors: vec![],
gpu_selected_wires: vec![],
gpu_hatches: vec![],
gpu_wipeouts: vec![],
@@ -607,6 +610,30 @@ impl Pipeline {
.collect();
}
/// 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])
}).collect();
}
/// Upload all 3DFACE entities as two batched GPU objects:
/// - `gpu_face3d_fill`: filled triangles (1 buffer, 1 draw call)
/// - `gpu_face3d_edges`: merged edge wires (1 buffer, 1 draw call)
@@ -875,11 +902,27 @@ 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.wire_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for wire in &self.gpu_wires {
if wire.vertex_count >= 6 {
pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..));
pass.draw(0..wire.vertex_count, 0..1);
let mut scissor_active = false;
for (i, wire) in self.gpu_wires.iter().enumerate() {
if wire.vertex_count < 6 {
continue;
}
match self.wire_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_vertex_buffer(0, wire.vertex_buffer.slice(..));
pass.draw(0..wire.vertex_count, 0..1);
}
if scissor_active {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
}
}
+8 -1
View File
@@ -111,11 +111,16 @@ impl WireVertex {
pub struct WireGpu {
pub vertex_buffer: wgpu::Buffer,
pub vertex_count: u32,
/// Paper-space bbox [x0, y0, x1, y1] for GPU scissor clipping.
/// Set only for viewport-projected wires; None for regular wires.
pub vp_scissor: Option<[f32; 4]>,
}
impl WireGpu {
pub fn new(device: &wgpu::Device, wire: &WireModel) -> Self {
Self::build(device, wire, wire.color)
let mut g = Self::build(device, wire, wire.color);
g.vp_scissor = wire.vp_scissor;
g
}
/// Creates a ghost copy with `alpha` applied on top of the wire's own alpha.
@@ -209,6 +214,7 @@ impl WireGpu {
Self {
vertex_buffer,
vertex_count: chunk.len() as u32,
vp_scissor: None,
}
})
.collect()
@@ -310,6 +316,7 @@ impl WireGpu {
Self {
vertex_buffer,
vertex_count: vertices.len() as u32,
vp_scissor: None,
}
}
}
+1
View File
@@ -131,6 +131,7 @@ impl shader::Primitive for Primitive {
pipeline.upload_face3d(device, &self.face3d_wires[..]);
pipeline.cached_epoch = self.geometry_epoch;
}
pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
if self.show_viewcube {
pipeline.viewcube.upload(
queue,
+11
View File
@@ -105,6 +105,7 @@ pub fn tessellate(
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
};
}
@@ -134,6 +135,7 @@ pub fn tessellate(
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
};
}
_ => {}
@@ -161,6 +163,7 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
};
}
}
@@ -186,6 +189,7 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
};
}
}
@@ -217,6 +221,7 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
};
}
@@ -243,6 +248,7 @@ pub fn tessellate(
aci: 0,
key_vertices,
plinegen: false,
vp_scissor: None,
aabb: WireModel::UNBOUNDED_AABB,
};
}
@@ -274,6 +280,7 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
@@ -330,6 +337,7 @@ pub fn tessellate_dimension(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}];
if let Some(text) = dimension_text_entity(dim) {
@@ -379,6 +387,7 @@ fn tessellate_leader(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}];
}
@@ -448,6 +457,7 @@ fn tessellate_leader(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}]
}
@@ -558,6 +568,7 @@ fn tessellate_multileader(
key_vertices: key_verts,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}];
// Render text content as MText wire
+5
View File
@@ -63,6 +63,9 @@ pub struct WireModel {
/// When false the linetype pattern restarts at each NaN-separated segment
/// (DXF PLINEGEN=0). When true the pattern runs continuously (PLINEGEN=1).
pub plinegen: bool,
/// Paper-space bounding box [x0, y0, x1, y1] for GPU scissor clipping.
/// Set only for viewport-projected wires in paper-space layouts.
pub vp_scissor: Option<[f32; 4]>,
}
impl WireModel {
@@ -89,6 +92,7 @@ impl WireModel {
key_vertices: vec![],
aabb: Self::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
@@ -189,6 +193,7 @@ impl Default for WireModel {
key_vertices: Vec::new(),
aabb: Self::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
}
}
}