Another project
1use bone_types::{Angle, Length, Point2};
2use uom::si::angle::radian;
3use uom::si::length::millimeter;
4use wgpu::util::DeviceExt;
5
6use crate::RenderTargets;
7use crate::camera::Camera2;
8use crate::gpu::{Gpu, PICK_FORMAT};
9use crate::pick::PickId;
10use crate::pipelines::{CONSTRUCTION_BIT, FRAME_UNIFORM_SIZE, build_frame_uniform};
11use crate::preview::{PreviewArc, PreviewCircle, SketchPreview};
12use crate::scene::{SceneArc, SceneCircle, SketchScene};
13use crate::snapshot::Style;
14
15#[repr(C)]
16#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
17struct ArcInstance {
18 center: [f32; 2],
19 radius_mm: f32,
20 half_width_px: f32,
21 start_rad: f32,
22 sweep_rad: f32,
23 aabb_min_mm: [f32; 2],
24 aabb_max_mm: [f32; 2],
25 pick_id: u32,
26 style_bits: u32,
27}
28
29const INSTANCE_STRIDE: u64 = core::mem::size_of::<ArcInstance>() as u64;
30
31pub struct ArcPipeline {
32 device: wgpu::Device,
33 queue: wgpu::Queue,
34 pipeline: wgpu::RenderPipeline,
35 uniform_buffer: wgpu::Buffer,
36 bind_group: wgpu::BindGroup,
37}
38
39impl ArcPipeline {
40 #[must_use]
41 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
42 let device = gpu.device().clone();
43 let queue = gpu.queue().clone();
44 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
45 label: Some("bone-render:arc-shader"),
46 source: wgpu::ShaderSource::Wgsl(include_str!("arc.wgsl").into()),
47 });
48 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
49 label: Some("bone-render:arc-bgl"),
50 entries: &[wgpu::BindGroupLayoutEntry {
51 binding: 0,
52 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
53 ty: wgpu::BindingType::Buffer {
54 ty: wgpu::BufferBindingType::Uniform,
55 has_dynamic_offset: false,
56 min_binding_size: wgpu::BufferSize::new(FRAME_UNIFORM_SIZE),
57 },
58 count: None,
59 }],
60 });
61 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
62 label: Some("bone-render:arc-layout"),
63 bind_group_layouts: &[Some(&bind_group_layout)],
64 immediate_size: 0,
65 });
66 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
67 label: Some("bone-render:arc-pipeline"),
68 layout: Some(&pipeline_layout),
69 vertex: wgpu::VertexState {
70 module: &shader,
71 entry_point: Some("vs"),
72 compilation_options: wgpu::PipelineCompilationOptions::default(),
73 buffers: &[wgpu::VertexBufferLayout {
74 array_stride: INSTANCE_STRIDE,
75 step_mode: wgpu::VertexStepMode::Instance,
76 attributes: &INSTANCE_ATTRS,
77 }],
78 },
79 fragment: Some(wgpu::FragmentState {
80 module: &shader,
81 entry_point: Some("fs"),
82 compilation_options: wgpu::PipelineCompilationOptions::default(),
83 targets: &[
84 Some(wgpu::ColorTargetState {
85 format: color_format,
86 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
87 write_mask: wgpu::ColorWrites::ALL,
88 }),
89 Some(wgpu::ColorTargetState {
90 format: PICK_FORMAT,
91 blend: None,
92 write_mask: wgpu::ColorWrites::ALL,
93 }),
94 ],
95 }),
96 primitive: wgpu::PrimitiveState {
97 topology: wgpu::PrimitiveTopology::TriangleList,
98 strip_index_format: None,
99 front_face: wgpu::FrontFace::Ccw,
100 cull_mode: None,
101 polygon_mode: wgpu::PolygonMode::Fill,
102 conservative: false,
103 unclipped_depth: false,
104 },
105 depth_stencil: None,
106 multisample: wgpu::MultisampleState::default(),
107 multiview_mask: None,
108 cache: None,
109 });
110 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
111 label: Some("bone-render:arc-uniform"),
112 size: FRAME_UNIFORM_SIZE,
113 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
114 mapped_at_creation: false,
115 });
116 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
117 label: Some("bone-render:arc-bg"),
118 layout: &bind_group_layout,
119 entries: &[wgpu::BindGroupEntry {
120 binding: 0,
121 resource: uniform_buffer.as_entire_binding(),
122 }],
123 });
124 Self {
125 device,
126 queue,
127 pipeline,
128 uniform_buffer,
129 bind_group,
130 }
131 }
132
133 pub fn draw(
134 &self,
135 encoder: &mut wgpu::CommandEncoder,
136 targets: RenderTargets<'_>,
137 camera: Camera2,
138 style: &Style,
139 scene: &SketchScene,
140 preview: &SketchPreview,
141 ) {
142 let instances = build_instances(scene, preview, style);
143 if instances.is_empty() {
144 return;
145 }
146 let uniform = build_frame_uniform(camera, style);
147 self.queue
148 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
149 let instance_buffer = self
150 .device
151 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
152 label: Some("bone-render:arc-instances"),
153 contents: bytemuck::cast_slice(&instances),
154 usage: wgpu::BufferUsages::VERTEX,
155 });
156 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
157 label: Some("bone-render:arc-pass"),
158 color_attachments: &[
159 Some(wgpu::RenderPassColorAttachment {
160 view: targets.color,
161 resolve_target: None,
162 depth_slice: None,
163 ops: wgpu::Operations {
164 load: wgpu::LoadOp::Load,
165 store: wgpu::StoreOp::Store,
166 },
167 }),
168 Some(wgpu::RenderPassColorAttachment {
169 view: targets.pick,
170 resolve_target: None,
171 depth_slice: None,
172 ops: wgpu::Operations {
173 load: wgpu::LoadOp::Load,
174 store: wgpu::StoreOp::Store,
175 },
176 }),
177 ],
178 depth_stencil_attachment: None,
179 timestamp_writes: None,
180 occlusion_query_set: None,
181 multiview_mask: None,
182 });
183 pass.set_pipeline(&self.pipeline);
184 pass.set_bind_group(0, &self.bind_group, &[]);
185 pass.set_vertex_buffer(0, instance_buffer.slice(..));
186 let len = instances.len();
187 let Ok(count) = u32::try_from(len) else {
188 panic!("arc instance count {len} exceeds u32::MAX");
189 };
190 pass.draw(0..6, 0..count);
191 }
192}
193
194impl core::fmt::Debug for ArcPipeline {
195 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196 f.debug_struct("ArcPipeline").finish_non_exhaustive()
197 }
198}
199
200const INSTANCE_ATTRS: [wgpu::VertexAttribute; 9] = wgpu::vertex_attr_array![
201 0 => Float32x2,
202 1 => Float32,
203 2 => Float32,
204 3 => Float32,
205 4 => Float32,
206 5 => Float32x2,
207 6 => Float32x2,
208 7 => Uint32,
209 8 => Uint32,
210];
211
212const FULL_CIRCLE_SWEEP: f32 = core::f32::consts::TAU;
213
214fn build_instances(
215 scene: &SketchScene,
216 preview: &SketchPreview,
217 style: &Style,
218) -> Vec<ArcInstance> {
219 let arcs = scene.arcs().iter().map(|a| arc_instance(*a, style));
220 let circles = scene.circles().iter().map(|c| circle_instance(*c, style));
221 let preview_circles = preview
222 .circles
223 .iter()
224 .map(|c| preview_circle_instance(*c, style));
225 let preview_arcs = preview.arcs.iter().map(|a| preview_arc_instance(*a, style));
226 arcs.chain(circles)
227 .chain(preview_circles)
228 .chain(preview_arcs)
229 .collect()
230}
231
232#[allow(
233 clippy::cast_possible_truncation,
234 reason = "mm coordinates and radians fit f32 mantissa at CAD scales"
235)]
236fn arc_instance(arc: SceneArc, style: &Style) -> ArcInstance {
237 let (cx, cy) = arc.center().coords_mm();
238 let radius_mm = arc.radius().get::<millimeter>() as f32;
239 let start_rad = arc.start_angle().get::<radian>() as f32;
240 let sweep_rad = arc.sweep_angle().get::<radian>() as f32;
241 let (aabb_min_mm, aabb_max_mm) = arc_aabb_offsets_mm(
242 arc.center(),
243 arc.radius(),
244 arc.start_angle(),
245 arc.sweep_angle(),
246 );
247 let bits = if arc.for_construction() {
248 CONSTRUCTION_BIT
249 } else {
250 0
251 };
252 ArcInstance {
253 center: [cx as f32, cy as f32],
254 radius_mm,
255 half_width_px: style.strokes().stroke_width_px() * 0.5,
256 start_rad,
257 sweep_rad,
258 aabb_min_mm,
259 aabb_max_mm,
260 pick_id: arc.pick().raw(),
261 style_bits: bits,
262 }
263}
264
265#[allow(
266 clippy::cast_possible_truncation,
267 reason = "mm coordinates and radians fit f32 mantissa at CAD scales"
268)]
269fn circle_instance(circle: SceneCircle, style: &Style) -> ArcInstance {
270 let (cx, cy) = circle.center().coords_mm();
271 let radius_mm = circle.radius().get::<millimeter>() as f32;
272 let bits = if circle.for_construction() {
273 CONSTRUCTION_BIT
274 } else {
275 0
276 };
277 ArcInstance {
278 center: [cx as f32, cy as f32],
279 radius_mm,
280 half_width_px: style.strokes().stroke_width_px() * 0.5,
281 start_rad: 0.0,
282 sweep_rad: FULL_CIRCLE_SWEEP,
283 aabb_min_mm: [-radius_mm, -radius_mm],
284 aabb_max_mm: [radius_mm, radius_mm],
285 pick_id: circle.pick().raw(),
286 style_bits: bits,
287 }
288}
289
290#[allow(
291 clippy::cast_possible_truncation,
292 reason = "mm coordinates and radians fit f32 mantissa at CAD scales"
293)]
294fn preview_circle_instance(circle: PreviewCircle, style: &Style) -> ArcInstance {
295 let (cx, cy) = circle.center.coords_mm();
296 let radius_mm = circle.radius.get::<millimeter>() as f32;
297 ArcInstance {
298 center: [cx as f32, cy as f32],
299 radius_mm,
300 half_width_px: style.strokes().stroke_width_px() * 0.5,
301 start_rad: 0.0,
302 sweep_rad: FULL_CIRCLE_SWEEP,
303 aabb_min_mm: [-radius_mm, -radius_mm],
304 aabb_max_mm: [radius_mm, radius_mm],
305 pick_id: PickId::NONE.raw(),
306 style_bits: 0,
307 }
308}
309
310#[allow(
311 clippy::cast_possible_truncation,
312 reason = "mm coordinates and radians fit f32 mantissa at CAD scales"
313)]
314fn preview_arc_instance(arc: PreviewArc, style: &Style) -> ArcInstance {
315 let (cx, cy) = arc.center.coords_mm();
316 let radius_mm = arc.radius.get::<millimeter>() as f32;
317 let start_rad = arc.start_angle.get::<radian>() as f32;
318 let sweep_rad = arc.sweep_angle.get::<radian>() as f32;
319 let (aabb_min_mm, aabb_max_mm) =
320 arc_aabb_offsets_mm(arc.center, arc.radius, arc.start_angle, arc.sweep_angle);
321 ArcInstance {
322 center: [cx as f32, cy as f32],
323 radius_mm,
324 half_width_px: style.strokes().stroke_width_px() * 0.5,
325 start_rad,
326 sweep_rad,
327 aabb_min_mm,
328 aabb_max_mm,
329 pick_id: PickId::NONE.raw(),
330 style_bits: 0,
331 }
332}
333
334#[allow(
335 clippy::cast_possible_truncation,
336 reason = "mm coordinates and radians fit f32 mantissa at CAD scales"
337)]
338fn arc_aabb_offsets_mm(
339 center: Point2,
340 radius: Length,
341 start: Angle,
342 sweep: Angle,
343) -> ([f32; 2], [f32; 2]) {
344 let aabb = bone_kernel::arc_bounding_box(center, radius, start, sweep);
345 let (cx, cy) = center.coords_mm();
346 let (mnx, mny) = aabb.min().coords_mm();
347 let (mxx, mxy) = aabb.max().coords_mm();
348 (
349 [(mnx - cx) as f32, (mny - cy) as f32],
350 [(mxx - cx) as f32, (mxy - cy) as f32],
351 )
352}
353
354#[cfg(test)]
355mod tests {
356 use super::arc_aabb_offsets_mm;
357 use bone_types::{Angle, Length, Point2};
358 use core::f64::consts::FRAC_PI_2;
359 use uom::si::angle::radian;
360 use uom::si::length::millimeter;
361
362 #[test]
363 fn quarter_arc_at_origin_offsets_to_unit_quadrant() {
364 let (min, max) = arc_aabb_offsets_mm(
365 Point2::from_mm(0.0, 0.0),
366 Length::new::<millimeter>(1.0),
367 Angle::new::<radian>(0.0),
368 Angle::new::<radian>(FRAC_PI_2),
369 );
370 assert!(min[0].abs() < 1e-5 && min[1].abs() < 1e-5);
371 assert!((max[0] - 1.0).abs() < 1e-5 && (max[1] - 1.0).abs() < 1e-5);
372 }
373
374 #[test]
375 fn translated_center_yields_the_same_relative_offsets() {
376 let (min, max) = arc_aabb_offsets_mm(
377 Point2::from_mm(10.0, -5.0),
378 Length::new::<millimeter>(1.0),
379 Angle::new::<radian>(0.0),
380 Angle::new::<radian>(FRAC_PI_2),
381 );
382 assert!(min[0].abs() < 1e-5 && min[1].abs() < 1e-5);
383 assert!((max[0] - 1.0).abs() < 1e-5 && (max[1] - 1.0).abs() < 1e-5);
384 }
385}