forked from 1b-agent/opencadstudio-upstream
fix(viewport): clip non-rectangular paper viewports to their boundary shape
Paper content viewports with a circle / spline / ellipse clip boundary drew their model content to the full bounding rectangle instead of the shape. Each paper content viewport is its own render into its own screen rectangle, so clipping is done per viewport: the boundary polygon is projected into that viewport's render-target NDC (paper shape mapped through the same visible-sub-rect crop as the content, so it stays fixed to the viewport frame) and stamped into the stencil buffer with an even-odd Invert, which marks the interior of any shape, convex or not, without triangulation. Every content pass then draws only where the stencil is set, so hatch, wire, mesh, 3DFACE, text, image, wipeout and selection are all clipped uniformly. Because Invert turns the cleared 0x00 into 0xFF, clipped content draws with stencil reference 0xFF (unclipped content keeps reference 0). The shared depth buffer moves to Depth24PlusStencil8 across every pipeline. Rectangular viewports keep clipping via their own render rectangle and XCLIP keeps its existing CPU wire clip; neither needs the stencil path. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
1b6a3a6f5b
commit
c97c2a8250
6 files changed
+378
-41
No files matched your search
+208
-38
@@ -39,6 +39,8 @@ const MSAA_SAMPLES: u32 = 4;
|
||||
|
||||
pub struct Pipeline {
|
||||
wire_pipeline: wgpu::RenderPipeline,
|
||||
/// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP).
|
||||
clip_mask_pipeline: wgpu::RenderPipeline,
|
||||
/// Black-fragment variant of `wire_pipeline` for 3D mesh outline edges in
|
||||
/// filled render modes.
|
||||
wire_black_pipeline: wgpu::RenderPipeline,
|
||||
@@ -169,6 +171,12 @@ pub struct Pipeline {
|
||||
pub(crate) wire_arena_id: u64,
|
||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame.
|
||||
wire_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||
/// This content viewport's non-rectangular clip boundary as a triangle-fan
|
||||
/// vertex buffer in the render target's normalized device coords (`None` =
|
||||
/// rectangular / unclipped, where the viewport's own render rectangle does
|
||||
/// the clipping). Stamped into the stencil once per frame; every content
|
||||
/// pass then draws with stencil reference 1 so only the interior survives.
|
||||
clip_boundary: Option<(wgpu::Buffer, u32)>,
|
||||
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
||||
gpu_selected_wires: Vec<WireGpu>,
|
||||
/// Command-preview / interim / grip-drag overlay wires. Re-uploaded every
|
||||
@@ -350,6 +358,25 @@ impl Pipeline {
|
||||
))),
|
||||
});
|
||||
|
||||
// Stencil test shared by every paper content pipeline: draw only where
|
||||
// the stencil equals the bound reference. Non-clipped content binds
|
||||
// reference 0 (matching the frame's stencil, cleared to 0); a clip
|
||||
// region's content binds reference 1 after its boundary has been stamped
|
||||
// into the stencil by the clip-mask pipeline. In Model space nothing
|
||||
// masks the stencil, so it stays 0 and reference 0 always passes.
|
||||
let content_stencil_face = wgpu::StencilFaceState {
|
||||
compare: wgpu::CompareFunction::Equal,
|
||||
fail_op: wgpu::StencilOperation::Keep,
|
||||
depth_fail_op: wgpu::StencilOperation::Keep,
|
||||
pass_op: wgpu::StencilOperation::Keep,
|
||||
};
|
||||
let content_stencil = wgpu::StencilState {
|
||||
front: content_stencil_face,
|
||||
back: content_stencil_face,
|
||||
read_mask: 0xff,
|
||||
write_mask: 0x00,
|
||||
};
|
||||
|
||||
let wire_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("wire.pipeline"),
|
||||
layout: Some(&wire_layout),
|
||||
@@ -365,10 +392,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
@@ -390,6 +417,85 @@ impl Pipeline {
|
||||
cache: None,
|
||||
});
|
||||
|
||||
// ── Clip-mask pipeline ─────────────────────────────────────────────
|
||||
// Stamps a viewport / XCLIP boundary polygon into the stencil buffer:
|
||||
// no colour, no depth, stencil `Invert` (even-odd fill → interior marked
|
||||
// for any polygon, convex or not). The boundary is drawn as a triangle
|
||||
// fan in paper coordinates and transformed exactly like wires.
|
||||
let clip_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("clip_mask.shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||
"../../shaders/clip_mask.wgsl"
|
||||
))),
|
||||
});
|
||||
let clip_mask_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("clip_mask.pipeline_layout"),
|
||||
bind_group_layouts: &[],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let clip_mask_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("clip_mask.pipeline"),
|
||||
layout: Some(&clip_mask_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &clip_mask_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[wgpu::VertexBufferLayout {
|
||||
array_stride: 8,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
}],
|
||||
}],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState {
|
||||
front: wgpu::StencilFaceState {
|
||||
compare: wgpu::CompareFunction::Always,
|
||||
fail_op: wgpu::StencilOperation::Keep,
|
||||
depth_fail_op: wgpu::StencilOperation::Keep,
|
||||
pass_op: wgpu::StencilOperation::Invert,
|
||||
},
|
||||
back: wgpu::StencilFaceState {
|
||||
compare: wgpu::CompareFunction::Always,
|
||||
fail_op: wgpu::StencilOperation::Keep,
|
||||
depth_fail_op: wgpu::StencilOperation::Keep,
|
||||
pass_op: wgpu::StencilOperation::Invert,
|
||||
},
|
||||
read_mask: 0xff,
|
||||
write_mask: 0xff,
|
||||
},
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: MSAA_SAMPLES,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &clip_mask_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::empty(),
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
// Black variant of `wire_pipeline` — same geometry/depth, black fragment
|
||||
// — for 3D mesh outline edges in filled modes (see the wire draw loop).
|
||||
let wire_black_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
@@ -407,10 +513,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
@@ -450,10 +556,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
@@ -529,10 +635,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -596,10 +702,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -667,10 +773,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -727,10 +833,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -776,10 +882,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -824,10 +930,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
@@ -872,10 +978,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
@@ -919,10 +1025,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -977,10 +1083,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -1024,10 +1130,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 1,
|
||||
slope_scale: 1.0,
|
||||
@@ -1118,10 +1224,10 @@ impl Pipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
@@ -1278,6 +1384,7 @@ impl Pipeline {
|
||||
|
||||
Self {
|
||||
wire_pipeline,
|
||||
clip_mask_pipeline,
|
||||
wire_black_pipeline,
|
||||
wire_xray_pipeline,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -1336,6 +1443,7 @@ impl Pipeline {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena_id: u64::MAX,
|
||||
wire_pixel_scissors: vec![],
|
||||
clip_boundary: None,
|
||||
gpu_selected_wires: vec![],
|
||||
gpu_preview_wires: vec![],
|
||||
gpu_hatch: None,
|
||||
@@ -1750,6 +1858,37 @@ impl Pipeline {
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Upload this content viewport's non-rectangular clip boundary as a
|
||||
/// triangle-fan vertex buffer in render-target NDC. The boundary is already
|
||||
/// projected (paper shape → the same visible-sub-rect crop as the content),
|
||||
/// so the mask pipeline stamps it straight into the stencil with `Invert`
|
||||
/// (even-odd fill → interior marked, any convexity). Empty input clears the
|
||||
/// boundary so the viewport renders unclipped (its render rectangle clips).
|
||||
pub fn upload_clip_boundary(&mut self, device: &wgpu::Device, boundary_ndc: &[[f32; 2]]) {
|
||||
use wgpu::util::DeviceExt;
|
||||
if boundary_ndc.len() < 3 {
|
||||
self.clip_boundary = None;
|
||||
return;
|
||||
}
|
||||
// Triangle fan (p0, pi, pi+1).
|
||||
let mut verts: Vec<f32> = Vec::with_capacity((boundary_ndc.len() - 2) * 6);
|
||||
for i in 1..boundary_ndc.len() - 1 {
|
||||
for &p in &[boundary_ndc[0], boundary_ndc[i], boundary_ndc[i + 1]] {
|
||||
verts.extend_from_slice(&[p[0], p[1]]);
|
||||
}
|
||||
}
|
||||
if verts.is_empty() {
|
||||
self.clip_boundary = None;
|
||||
return;
|
||||
}
|
||||
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("clip_boundary.vbuf"),
|
||||
contents: bytemuck::cast_slice(&verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
self.clip_boundary = Some((vbuf, (verts.len() / 2) as u32));
|
||||
}
|
||||
|
||||
/// Per-frame visibility refresh for the batched hatch path.
|
||||
/// Combines Phase 3.3 sub-pixel LOD skip with Phase 2.3 frustum
|
||||
/// cull and pushes the resulting 0/1 mask through to the GPU
|
||||
@@ -2065,6 +2204,13 @@ impl Pipeline {
|
||||
b: b as f64,
|
||||
a: a as f64,
|
||||
};
|
||||
// Non-rectangular viewport clip: the boundary is stamped into the
|
||||
// just-cleared (0x00) stencil with `Invert`, so an odd (interior)
|
||||
// coverage becomes 0xFF. Every content pass then draws with reference
|
||||
// 0xFF so only the interior survives. Rectangular / unclipped viewports
|
||||
// leave the stencil at 0 and draw with reference 0 (the viewport's own
|
||||
// render rectangle does the clipping).
|
||||
let stencil_ref: u32 = if self.clip_boundary.is_some() { 0xFF } else { 0 };
|
||||
|
||||
// Scene-render cache: when `prepare` found this frame pixel-identical
|
||||
// to the last (unchanged view / geometry / selection / preview), the
|
||||
@@ -2093,13 +2239,26 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
// Clip stencil starts at 0 (= "unclipped", passes content
|
||||
// bound to reference 0); clip masks stamp 1 into interiors.
|
||||
stencil_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
// MSAA texture is clip-bounds-sized, so viewport starts at (0, 0).
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
// Stamp the viewport clip boundary into the just-cleared stencil
|
||||
// (interior → 1) before any content draws, so every pass below can
|
||||
// clip to the shape with reference 1.
|
||||
if let Some((vbuf, vcount)) = &self.clip_boundary {
|
||||
pass.set_pipeline(&self.clip_mask_pipeline);
|
||||
pass.set_vertex_buffer(0, vbuf.slice(..));
|
||||
pass.draw(0..*vcount, 0..1);
|
||||
}
|
||||
// Phase 4-B — single batched draw covers every hatch.
|
||||
// Vertex shader culls per-instance via the `visibility`
|
||||
// buffer (sub-pixel LOD + frustum cull written each frame
|
||||
@@ -2113,6 +2272,7 @@ impl Pipeline {
|
||||
// hatch pass dominates the GPU frame on hatch-heavy drawings.
|
||||
if !self.skip_hatch_frame {
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_bind_group(1, &batch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
|
||||
@@ -2127,6 +2287,7 @@ impl Pipeline {
|
||||
if !self.skip_hatch_frame {
|
||||
pass.set_pipeline(&self.hatch_web_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
for hatch in &self.gpu_hatches_web {
|
||||
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
||||
@@ -2154,7 +2315,7 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -2162,6 +2323,7 @@ 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, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
let mut scissor_active = false;
|
||||
for (i, img) in self.gpu_images.iter().enumerate() {
|
||||
match self.image_pixel_scissors.get(i) {
|
||||
@@ -2203,13 +2365,14 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
// Four draw paths share this pass:
|
||||
// - Solid: `mesh_pipeline` + triangle index buf.
|
||||
// - Wireframe: `mesh_wireframe_pipeline` + the
|
||||
@@ -2382,13 +2545,14 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
if !fill.chunks_3d.is_empty() {
|
||||
if hidden_line {
|
||||
pass.set_pipeline(&self.face3d_depth_pipeline);
|
||||
@@ -2431,7 +2595,7 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -2439,6 +2603,7 @@ 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, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
for edges in &self.gpu_face3d_edges {
|
||||
if edges.instance_count > 0 {
|
||||
if let Some(bg) = &edges.const_bind_group {
|
||||
@@ -2469,7 +2634,7 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -2484,6 +2649,7 @@ impl Pipeline {
|
||||
let want_solid_with_edges = !hidden_line && !mesh_wireframe && show_3d_edges;
|
||||
let mut scissor_active = false;
|
||||
let mut black_active = false;
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
for (i, wire) in self.gpu_wires.iter().enumerate() {
|
||||
if wire.instance_count == 0 {
|
||||
continue;
|
||||
@@ -2498,6 +2664,7 @@ impl Pipeline {
|
||||
continue;
|
||||
}
|
||||
let use_black = want_solid_with_edges && wire.is_3d_mesh_edge;
|
||||
|
||||
if use_black != black_active {
|
||||
pass.set_pipeline(if use_black {
|
||||
&self.wire_black_pipeline
|
||||
@@ -2571,7 +2738,7 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -2579,6 +2746,7 @@ 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.text_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
pass.set_bind_group(1, &atlas.bind_group, &[]);
|
||||
if let Some(vbuf) = &self.text_vbuf {
|
||||
if self.text_vcount > 0 {
|
||||
@@ -2621,7 +2789,7 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -2629,6 +2797,7 @@ 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.wipeout_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
let mut scissor_active = false;
|
||||
for (i, wipeout) in self.gpu_wipeouts.iter().enumerate() {
|
||||
if self.wipeout_skip_flags.get(i).copied().unwrap_or(false) {
|
||||
@@ -2675,7 +2844,7 @@ impl Pipeline {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -2683,6 +2852,7 @@ 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_xray_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
for wire in &self.gpu_selected_wires {
|
||||
if wire.instance_count > 0 {
|
||||
if let Some(bg) = &wire.const_bind_group {
|
||||
@@ -2997,7 +3167,7 @@ fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture
|
||||
mip_level_count: 1,
|
||||
sample_count: MSAA_SAMPLES,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
})
|
||||
|
||||
@@ -293,7 +293,7 @@ pub fn create_pipeline(
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
|
||||
@@ -1187,7 +1187,7 @@ impl ViewCubePipeline {
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
@@ -1319,7 +1319,7 @@ fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
})
|
||||
|
||||
@@ -347,6 +347,127 @@ impl Scene {
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Clip regions for every on content viewport in `paper_block`: the paper
|
||||
/// bounding rect (matching the wires' `vp_scissor`) plus the boundary
|
||||
/// polygon whose interior the GPU keeps via the stencil buffer. A plain
|
||||
/// rectangular viewport contributes its four corners; a viewport with a
|
||||
/// non-rectangular clip (`clip_boundary_handle`) contributes that entity's
|
||||
/// tessellated outline (circle / ellipse / spline / polyline), reusing the
|
||||
/// entity's own `to_truck` tessellation.
|
||||
/// A content viewport's non-rectangular clip boundary, projected into that
|
||||
/// viewport's render-target normalized device coords. Returns an empty vec
|
||||
/// for a rectangular viewport (no `clip_boundary_handle`) — those clip via
|
||||
/// their own render rectangle and need no stencil mask.
|
||||
///
|
||||
/// `uo/vo/us/vs` are the visible-sub-rect crop the content camera applies
|
||||
/// (see `crop_view_proj`); the same crop is applied here so the boundary
|
||||
/// lines up with the content even when the viewport hangs off-canvas.
|
||||
pub(super) fn viewport_clip_boundary_ndc(
|
||||
&self,
|
||||
vp_handle: Handle,
|
||||
uo: f32,
|
||||
vo: f32,
|
||||
us: f32,
|
||||
vs: f32,
|
||||
) -> Vec<[f32; 2]> {
|
||||
use acadrust::entities::Viewport;
|
||||
let Some(EntityType::Viewport(vp)) = self.document.get_entity(vp_handle) else {
|
||||
return vec![];
|
||||
};
|
||||
let vp: &Viewport = vp;
|
||||
if vp.clip_boundary_handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
let pcx = vp.center.x as f32;
|
||||
let pcy = vp.center.y as f32;
|
||||
let z = vp.center.z as f32;
|
||||
let hw = (vp.width / 2.0) as f32;
|
||||
let hh = (vp.height / 2.0) as f32;
|
||||
if hw.abs() < 1e-6 || hh.abs() < 1e-6 {
|
||||
return vec![];
|
||||
}
|
||||
let poly = self.clip_boundary_polygon(vp.clip_boundary_handle, z);
|
||||
if poly.len() < 3 {
|
||||
return vec![];
|
||||
}
|
||||
// Paper point → full-rectangle NDC ([-1, 1] across the viewport rect,
|
||||
// Y up, matching the content camera + `viewport_screen_rect`), then the
|
||||
// same crop the content camera uses to map its visible sub-rect back to
|
||||
// NDC [-1, 1].
|
||||
let us = us.max(1e-6);
|
||||
let vs = vs.max(1e-6);
|
||||
let sx = 1.0 / us;
|
||||
let sy = 1.0 / vs;
|
||||
let tx = (1.0 - 2.0 * uo - us) / us;
|
||||
let ty = -(1.0 - 2.0 * vo - vs) / vs;
|
||||
poly.iter()
|
||||
.map(|p| {
|
||||
let nx = (p[0] - pcx) / hw;
|
||||
let ny = (p[1] - pcy) / hh;
|
||||
[sx * nx + tx, sy * ny + ty]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Tessellated outline of a non-rectangular viewport / XCLIP clip-boundary
|
||||
/// entity, in paper coordinates, by reusing the entity's own `to_truck`
|
||||
/// tessellation (Lines). NaN segment breaks are dropped so the result is a
|
||||
/// single ordered ring suitable for a stencil triangle-fan.
|
||||
pub(super) fn clip_boundary_polygon(&self, handle: Handle, z: f32) -> Vec<[f32; 3]> {
|
||||
use std::f64::consts::TAU;
|
||||
let Some(entity) = self
|
||||
.document
|
||||
.entities()
|
||||
.find(|e| e.common().handle == handle)
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
// Circles and ellipses tessellate directly — their `to_truck` returns a
|
||||
// parametric TruckObject (not `Lines`), so extracting a polygon there
|
||||
// would come back empty. Everything else (splines, polylines, …) reuses
|
||||
// the entity's own `Lines` tessellation.
|
||||
const N: usize = 64;
|
||||
match entity {
|
||||
EntityType::Circle(c) => (0..N)
|
||||
.map(|i| {
|
||||
let a = i as f64 * TAU / N as f64;
|
||||
[
|
||||
(c.center.x + a.cos() * c.radius) as f32,
|
||||
(c.center.y + a.sin() * c.radius) as f32,
|
||||
z,
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
EntityType::Ellipse(el) => {
|
||||
// major_axis = center → major endpoint; minor = perp(major) × ratio.
|
||||
let (mx, my) = (el.major_axis.x, el.major_axis.y);
|
||||
let r = el.minor_axis_ratio;
|
||||
(0..N)
|
||||
.map(|i| {
|
||||
let t = i as f64 * TAU / N as f64;
|
||||
let px = mx * t.cos() - my * r * t.sin();
|
||||
let py = my * t.cos() + mx * r * t.sin();
|
||||
[(el.center.x + px) as f32, (el.center.y + py) as f32, z]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let Some(te) = entity.to_truck_entity(&self.document) else {
|
||||
return vec![];
|
||||
};
|
||||
if let crate::scene::convert::acad_to_truck::TruckObject::Lines(pts) = te.object {
|
||||
pts.into_iter()
|
||||
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
||||
.map(|p| [p[0] as f32, p[1] as f32, z])
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paper boundary wire ────────────────────────────────────────────────────
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct ViewportData {
|
||||
/// reused by a different viewport and reset its (index-addressed) caches.
|
||||
pub(in crate::scene) instance_id: u64,
|
||||
pub(in crate::scene) wires: Arc<Vec<WireModel>>,
|
||||
/// This content viewport's non-rectangular clip boundary (paper layouts
|
||||
/// only), as a polygon already projected into the viewport's render-target
|
||||
/// NDC. The GPU stamps it into the stencil so content is clipped to the
|
||||
/// shape. Empty for rectangular viewports, the paper sheet, and Model space.
|
||||
pub(in crate::scene) clip_boundary_ndc: Arc<Vec<[f32; 2]>>,
|
||||
/// Live command-preview / interim / grip-drag overlay wires. Kept out of
|
||||
/// the main `wires` buffer so a drag re-uploads only this small set each
|
||||
/// frame, never the resident base buffer. Drawn on top in the wire pass.
|
||||
@@ -570,6 +575,7 @@ impl shader::Primitive for Primitive {
|
||||
} else {
|
||||
inner.upload_silhouettes(device, &[], vp.view_dir);
|
||||
}
|
||||
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
|
||||
inner.compute_wire_scissors(view_rot, eye, clip_size.width, clip_size.height);
|
||||
inner.compute_wipeout_scissors(view_rot, eye, clip_size.width, clip_size.height);
|
||||
inner.compute_image_scissors(view_rot, eye, clip_size.width, clip_size.height);
|
||||
@@ -1352,9 +1358,24 @@ impl Scene {
|
||||
} else {
|
||||
0x3000_0000_0000_0000 | inst.handle.value()
|
||||
};
|
||||
// A paper content viewport with a non-rectangular clip entity gets its
|
||||
// boundary projected into this render target's NDC (paper shape mapped
|
||||
// through the same visible-sub-rect crop as the content). Rectangular
|
||||
// viewports, the paper sheet and Model tiles clip via their own render
|
||||
// rectangle and need no stencil boundary.
|
||||
let clip_boundary_ndc = if !inst.paper_sheet
|
||||
&& inst.tile_idx.is_none()
|
||||
&& inst.handle != acadrust::Handle::NULL
|
||||
&& self.current_layout != "Model"
|
||||
{
|
||||
Arc::new(self.viewport_clip_boundary_ndc(inst.handle, uo, vo, us, vs))
|
||||
} else {
|
||||
Arc::new(vec![])
|
||||
};
|
||||
Some(ViewportData {
|
||||
instance_id,
|
||||
wires: all_wires,
|
||||
clip_boundary_ndc,
|
||||
preview_wires,
|
||||
face3d_wires,
|
||||
text_verts,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Viewport / XCLIP stencil-mask shader.
|
||||
//
|
||||
// Draws a clip-boundary polygon (as a triangle fan) into the stencil buffer so
|
||||
// a paper content viewport can clip its render to an arbitrary shape. It writes
|
||||
// no colour and no depth — only stencil (the pipeline uses `Invert`, so an
|
||||
// even-odd fill marks the polygon interior regardless of convexity).
|
||||
//
|
||||
// The boundary arrives already projected into the content viewport's normalized
|
||||
// device coordinates (paper-space shape mapped through the same visible-sub-rect
|
||||
// crop as the content), so the vertex stage is a pure pass-through — the mask
|
||||
// is fixed to the viewport frame and never moves with the model camera.
|
||||
|
||||
struct VIn {
|
||||
@location(0) ndc: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex fn vs_main(in: VIn) -> @builtin(position) vec4<f32> {
|
||||
return vec4<f32>(in.ndc, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Colour is write-masked off in the pipeline; this only satisfies the
|
||||
// fragment-output requirement.
|
||||
@fragment fn fs_main() -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
Reference in new issue
Block a user