Another project
1use wgpu::util::DeviceExt;
2
3use crate::RenderTargets;
4use crate::camera::Camera2;
5use crate::gpu::{Gpu, PICK_FORMAT};
6use crate::pick::PickId;
7use crate::pipelines::{CONSTRUCTION_BIT, FRAME_UNIFORM_SIZE, build_frame_uniform};
8use crate::preview::SketchPreview;
9use crate::scene::{SceneLine, ScenePoint, SketchScene};
10use crate::snapshot::Style;
11use bone_types::Point2;
12
13#[repr(C)]
14#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
15struct LineInstance {
16 a: [f32; 2],
17 b: [f32; 2],
18 half_width_px: f32,
19 pick_id: u32,
20 style_bits: u32,
21}
22
23const INSTANCE_STRIDE: u64 = core::mem::size_of::<LineInstance>() as u64;
24
25pub struct LinesPipeline {
26 device: wgpu::Device,
27 queue: wgpu::Queue,
28 pipeline: wgpu::RenderPipeline,
29 uniform_buffer: wgpu::Buffer,
30 bind_group: wgpu::BindGroup,
31}
32
33impl LinesPipeline {
34 #[must_use]
35 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
36 let device = gpu.device().clone();
37 let queue = gpu.queue().clone();
38 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
39 label: Some("bone-render:lines-shader"),
40 source: wgpu::ShaderSource::Wgsl(include_str!("lines.wgsl").into()),
41 });
42 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
43 label: Some("bone-render:lines-bgl"),
44 entries: &[wgpu::BindGroupLayoutEntry {
45 binding: 0,
46 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
47 ty: wgpu::BindingType::Buffer {
48 ty: wgpu::BufferBindingType::Uniform,
49 has_dynamic_offset: false,
50 min_binding_size: wgpu::BufferSize::new(FRAME_UNIFORM_SIZE),
51 },
52 count: None,
53 }],
54 });
55 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
56 label: Some("bone-render:lines-layout"),
57 bind_group_layouts: &[Some(&bind_group_layout)],
58 immediate_size: 0,
59 });
60 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
61 label: Some("bone-render:lines-pipeline"),
62 layout: Some(&pipeline_layout),
63 vertex: wgpu::VertexState {
64 module: &shader,
65 entry_point: Some("vs"),
66 compilation_options: wgpu::PipelineCompilationOptions::default(),
67 buffers: &[wgpu::VertexBufferLayout {
68 array_stride: INSTANCE_STRIDE,
69 step_mode: wgpu::VertexStepMode::Instance,
70 attributes: &INSTANCE_ATTRS,
71 }],
72 },
73 fragment: Some(wgpu::FragmentState {
74 module: &shader,
75 entry_point: Some("fs"),
76 compilation_options: wgpu::PipelineCompilationOptions::default(),
77 targets: &[
78 Some(wgpu::ColorTargetState {
79 format: color_format,
80 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
81 write_mask: wgpu::ColorWrites::ALL,
82 }),
83 Some(wgpu::ColorTargetState {
84 format: PICK_FORMAT,
85 blend: None,
86 write_mask: wgpu::ColorWrites::ALL,
87 }),
88 ],
89 }),
90 primitive: wgpu::PrimitiveState {
91 topology: wgpu::PrimitiveTopology::TriangleList,
92 strip_index_format: None,
93 front_face: wgpu::FrontFace::Ccw,
94 cull_mode: None,
95 polygon_mode: wgpu::PolygonMode::Fill,
96 conservative: false,
97 unclipped_depth: false,
98 },
99 depth_stencil: None,
100 multisample: wgpu::MultisampleState::default(),
101 multiview_mask: None,
102 cache: None,
103 });
104 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
105 label: Some("bone-render:lines-uniform"),
106 size: FRAME_UNIFORM_SIZE,
107 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
108 mapped_at_creation: false,
109 });
110 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
111 label: Some("bone-render:lines-bg"),
112 layout: &bind_group_layout,
113 entries: &[wgpu::BindGroupEntry {
114 binding: 0,
115 resource: uniform_buffer.as_entire_binding(),
116 }],
117 });
118 Self {
119 device,
120 queue,
121 pipeline,
122 uniform_buffer,
123 bind_group,
124 }
125 }
126
127 pub fn draw(
128 &self,
129 encoder: &mut wgpu::CommandEncoder,
130 targets: RenderTargets<'_>,
131 camera: Camera2,
132 style: &Style,
133 scene: &SketchScene,
134 preview: &SketchPreview,
135 ) {
136 let instances = build_instances(scene, preview, style);
137 if instances.is_empty() {
138 return;
139 }
140 let uniform = build_frame_uniform(camera, style);
141 self.queue
142 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
143 let instance_buffer = self
144 .device
145 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
146 label: Some("bone-render:lines-instances"),
147 contents: bytemuck::cast_slice(&instances),
148 usage: wgpu::BufferUsages::VERTEX,
149 });
150 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
151 label: Some("bone-render:lines-pass"),
152 color_attachments: &[
153 Some(wgpu::RenderPassColorAttachment {
154 view: targets.color,
155 resolve_target: None,
156 depth_slice: None,
157 ops: wgpu::Operations {
158 load: wgpu::LoadOp::Load,
159 store: wgpu::StoreOp::Store,
160 },
161 }),
162 Some(wgpu::RenderPassColorAttachment {
163 view: targets.pick,
164 resolve_target: None,
165 depth_slice: None,
166 ops: wgpu::Operations {
167 load: wgpu::LoadOp::Load,
168 store: wgpu::StoreOp::Store,
169 },
170 }),
171 ],
172 depth_stencil_attachment: None,
173 timestamp_writes: None,
174 occlusion_query_set: None,
175 multiview_mask: None,
176 });
177 pass.set_pipeline(&self.pipeline);
178 pass.set_bind_group(0, &self.bind_group, &[]);
179 pass.set_vertex_buffer(0, instance_buffer.slice(..));
180 let len = instances.len();
181 let Ok(count) = u32::try_from(len) else {
182 panic!("line instance count {len} exceeds u32::MAX");
183 };
184 pass.draw(0..6, 0..count);
185 }
186}
187
188impl core::fmt::Debug for LinesPipeline {
189 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190 f.debug_struct("LinesPipeline").finish_non_exhaustive()
191 }
192}
193
194const INSTANCE_ATTRS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![
195 0 => Float32x2,
196 1 => Float32x2,
197 2 => Float32,
198 3 => Uint32,
199 4 => Uint32,
200];
201
202fn build_instances(
203 scene: &SketchScene,
204 preview: &SketchPreview,
205 style: &Style,
206) -> Vec<LineInstance> {
207 let lines = scene.lines().iter().map(|l| line_instance(*l, style));
208 let points = scene.points().iter().map(|p| point_instance(*p, style));
209 let preview_segments = preview
210 .segments
211 .iter()
212 .map(|(a, b)| preview_line_instance(*a, *b, style));
213 let preview_anchors = preview
214 .anchors
215 .iter()
216 .map(|p| preview_point_instance(*p, style));
217 let preview_snap = preview
218 .snap
219 .iter()
220 .map(|p| preview_snap_instance(*p, style));
221 lines
222 .chain(points)
223 .chain(preview_segments)
224 .chain(preview_anchors)
225 .chain(preview_snap)
226 .collect()
227}
228
229#[allow(
230 clippy::cast_possible_truncation,
231 reason = "mm coordinates fit f32 mantissa at CAD scales"
232)]
233fn line_instance(line: SceneLine, style: &Style) -> LineInstance {
234 let (ax, ay) = line.a().coords_mm();
235 let (bx, by) = line.b().coords_mm();
236 let bits = if line.for_construction() {
237 CONSTRUCTION_BIT
238 } else {
239 0
240 };
241 LineInstance {
242 a: [ax as f32, ay as f32],
243 b: [bx as f32, by as f32],
244 half_width_px: style.strokes().stroke_width_px() * 0.5,
245 pick_id: line.pick().raw(),
246 style_bits: bits,
247 }
248}
249
250#[allow(
251 clippy::cast_possible_truncation,
252 reason = "mm coordinates fit f32 mantissa at CAD scales"
253)]
254fn point_instance(point: ScenePoint, style: &Style) -> LineInstance {
255 let (x, y) = point.at().coords_mm();
256 let xy = [x as f32, y as f32];
257 LineInstance {
258 a: xy,
259 b: xy,
260 half_width_px: style.strokes().point_radius_px(),
261 pick_id: point.pick().raw(),
262 style_bits: 0,
263 }
264}
265
266#[allow(
267 clippy::cast_possible_truncation,
268 reason = "mm coordinates fit f32 mantissa at CAD scales"
269)]
270fn preview_line_instance(a: Point2, b: Point2, style: &Style) -> LineInstance {
271 let (ax, ay) = a.coords_mm();
272 let (bx, by) = b.coords_mm();
273 LineInstance {
274 a: [ax as f32, ay as f32],
275 b: [bx as f32, by as f32],
276 half_width_px: style.strokes().stroke_width_px() * 0.5,
277 pick_id: PickId::NONE.raw(),
278 style_bits: 0,
279 }
280}
281
282#[allow(
283 clippy::cast_possible_truncation,
284 reason = "mm coordinates fit f32 mantissa at CAD scales"
285)]
286fn preview_point_instance(at: Point2, style: &Style) -> LineInstance {
287 let (x, y) = at.coords_mm();
288 let xy = [x as f32, y as f32];
289 LineInstance {
290 a: xy,
291 b: xy,
292 half_width_px: style.strokes().point_radius_px(),
293 pick_id: PickId::NONE.raw(),
294 style_bits: 0,
295 }
296}
297
298const SNAP_RADIUS_RATIO: f32 = 1.8;
299
300#[allow(
301 clippy::cast_possible_truncation,
302 reason = "mm coordinates fit f32 mantissa at CAD scales"
303)]
304fn preview_snap_instance(at: Point2, style: &Style) -> LineInstance {
305 let (x, y) = at.coords_mm();
306 let xy = [x as f32, y as f32];
307 LineInstance {
308 a: xy,
309 b: xy,
310 half_width_px: style.strokes().point_radius_px() * SNAP_RADIUS_RATIO,
311 pick_id: PickId::NONE.raw(),
312 style_bits: CONSTRUCTION_BIT,
313 }
314}