1use bytemuck::{Pod, Zeroable};
43use wgpu::util::DeviceExt;
44
45use super::gpu::GpuContext;
46
47#[repr(C)]
49#[derive(Copy, Clone, Pod, Zeroable)]
50pub struct GeoVertex {
51 pub position: [f32; 2],
52 pub color: [f32; 4],
53}
54
55const MAX_VERTICES: usize = 65536;
58
59pub struct GeometryBatch {
60 pipeline: wgpu::RenderPipeline,
61 vertices: Vec<GeoVertex>,
62}
63
64impl GeometryBatch {
65 pub fn new_headless(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
67 Self::new_internal(device, format)
68 }
69
70 pub fn new(gpu: &GpuContext) -> Self {
75 Self::new_internal(&gpu.device, gpu.config.format)
76 }
77
78 fn new_internal(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
79 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
80 label: Some("geom_shader"),
81 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/geom.wgsl").into()),
82 });
83
84 let camera_bgl =
85 device
86 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
87 label: Some("geom_camera_bind_group_layout"),
88 entries: &[wgpu::BindGroupLayoutEntry {
89 binding: 0,
90 visibility: wgpu::ShaderStages::VERTEX,
91 ty: wgpu::BindingType::Buffer {
92 ty: wgpu::BufferBindingType::Uniform,
93 has_dynamic_offset: false,
94 min_binding_size: None,
95 },
96 count: None,
97 }],
98 });
99
100 let pipeline_layout =
101 device
102 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
103 label: Some("geom_pipeline_layout"),
104 bind_group_layouts: &[&camera_bgl],
105 push_constant_ranges: &[],
106 });
107
108 let vertex_layout = wgpu::VertexBufferLayout {
109 array_stride: std::mem::size_of::<GeoVertex>() as wgpu::BufferAddress,
110 step_mode: wgpu::VertexStepMode::Vertex,
111 attributes: &[
112 wgpu::VertexAttribute {
114 offset: 0,
115 shader_location: 0,
116 format: wgpu::VertexFormat::Float32x2,
117 },
118 wgpu::VertexAttribute {
120 offset: 8,
121 shader_location: 1,
122 format: wgpu::VertexFormat::Float32x4,
123 },
124 ],
125 };
126
127 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
128 label: Some("geom_pipeline"),
129 layout: Some(&pipeline_layout),
130 vertex: wgpu::VertexState {
131 module: &shader,
132 entry_point: Some("vs_main"),
133 buffers: &[vertex_layout],
134 compilation_options: Default::default(),
135 },
136 fragment: Some(wgpu::FragmentState {
137 module: &shader,
138 entry_point: Some("fs_main"),
139 targets: &[Some(wgpu::ColorTargetState {
140 format: surface_format,
141 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
142 write_mask: wgpu::ColorWrites::ALL,
143 })],
144 compilation_options: Default::default(),
145 }),
146 primitive: wgpu::PrimitiveState {
147 topology: wgpu::PrimitiveTopology::TriangleList,
148 strip_index_format: None,
149 front_face: wgpu::FrontFace::Ccw,
150 cull_mode: None,
151 polygon_mode: wgpu::PolygonMode::Fill,
152 unclipped_depth: false,
153 conservative: false,
154 },
155 depth_stencil: None,
156 multisample: wgpu::MultisampleState::default(),
157 multiview: None,
158 cache: None,
159 });
160
161 Self {
162 pipeline,
163 vertices: Vec::with_capacity(MAX_VERTICES),
164 }
165 }
166
167 pub fn add_triangle(
169 &mut self,
170 x1: f32, y1: f32,
171 x2: f32, y2: f32,
172 x3: f32, y3: f32,
173 r: f32, g: f32, b: f32, a: f32,
174 ) {
175 if self.vertices.len() + 3 > MAX_VERTICES {
176 return; }
178 let color = [r, g, b, a];
179 self.vertices.push(GeoVertex { position: [x1, y1], color });
180 self.vertices.push(GeoVertex { position: [x2, y2], color });
181 self.vertices.push(GeoVertex { position: [x3, y3], color });
182 }
183
184 pub fn add_line(
187 &mut self,
188 x1: f32, y1: f32,
189 x2: f32, y2: f32,
190 thickness: f32,
191 r: f32, g: f32, b: f32, a: f32,
192 ) {
193 if self.vertices.len() + 6 > MAX_VERTICES {
194 return;
195 }
196 let dx = x2 - x1;
197 let dy = y2 - y1;
198 let len = (dx * dx + dy * dy).sqrt();
199 if len < 1e-8 {
200 return; }
202 let half = thickness * 0.5;
204 let nx = -dy / len * half;
205 let ny = dx / len * half;
206
207 let color = [r, g, b, a];
208 let a0 = GeoVertex { position: [x1 + nx, y1 + ny], color };
210 let b0 = GeoVertex { position: [x1 - nx, y1 - ny], color };
211 let c0 = GeoVertex { position: [x2 - nx, y2 - ny], color };
212 let d0 = GeoVertex { position: [x2 + nx, y2 + ny], color };
213
214 self.vertices.push(a0);
216 self.vertices.push(b0);
217 self.vertices.push(c0);
218 self.vertices.push(a0);
219 self.vertices.push(c0);
220 self.vertices.push(d0);
221 }
222
223 pub fn flush(
226 &mut self,
227 device: &wgpu::Device,
228 encoder: &mut wgpu::CommandEncoder,
229 target: &wgpu::TextureView,
230 camera_bind_group: &wgpu::BindGroup,
231 ) {
232 if self.vertices.is_empty() {
233 return;
234 }
235
236 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
237 label: Some("geom_vertex_buffer"),
238 contents: bytemuck::cast_slice(&self.vertices),
239 usage: wgpu::BufferUsages::VERTEX,
240 });
241
242 let vertex_count = self.vertices.len() as u32;
243
244 {
245 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
246 label: Some("geom_render_pass"),
247 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
248 view: target,
249 resolve_target: None,
250 ops: wgpu::Operations {
251 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
253 },
254 })],
255 depth_stencil_attachment: None,
256 timestamp_writes: None,
257 occlusion_query_set: None,
258 });
259
260 pass.set_pipeline(&self.pipeline);
261 pass.set_bind_group(0, camera_bind_group, &[]);
262 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
263 pass.draw(0..vertex_count, 0..1);
264 }
265
266 self.vertices.clear();
267 }
268
269 pub fn flush_commands(
274 &mut self,
275 device: &wgpu::Device,
276 encoder: &mut wgpu::CommandEncoder,
277 target: &wgpu::TextureView,
278 camera_bind_group: &wgpu::BindGroup,
279 commands: &[crate::scripting::geometry_ops::GeoCommand],
280 clear_color: Option<wgpu::Color>,
281 ) {
282 if commands.is_empty() {
283 return;
284 }
285
286 let mut verts: Vec<GeoVertex> = Vec::new();
288 for cmd in commands {
289 match cmd {
290 crate::scripting::geometry_ops::GeoCommand::Triangle {
291 x1, y1, x2, y2, x3, y3, r, g, b, a, ..
292 } => {
293 let color = [*r, *g, *b, *a];
294 verts.push(GeoVertex { position: [*x1, *y1], color });
295 verts.push(GeoVertex { position: [*x2, *y2], color });
296 verts.push(GeoVertex { position: [*x3, *y3], color });
297 }
298 crate::scripting::geometry_ops::GeoCommand::LineSeg {
299 x1, y1, x2, y2, thickness, r, g, b, a, ..
300 } => {
301 let dx = x2 - x1;
302 let dy = y2 - y1;
303 let len = (dx * dx + dy * dy).sqrt();
304 if len < 1e-8 {
305 continue;
306 }
307 let half = thickness * 0.5;
308 let nx = -dy / len * half;
309 let ny = dx / len * half;
310 let color = [*r, *g, *b, *a];
311 let a0 = GeoVertex { position: [x1 + nx, y1 + ny], color };
312 let b0 = GeoVertex { position: [x1 - nx, y1 - ny], color };
313 let c0 = GeoVertex { position: [x2 - nx, y2 - ny], color };
314 let d0 = GeoVertex { position: [x2 + nx, y2 + ny], color };
315 verts.push(a0);
316 verts.push(b0);
317 verts.push(c0);
318 verts.push(a0);
319 verts.push(c0);
320 verts.push(d0);
321 }
322 }
323 }
324
325 if verts.is_empty() {
326 return;
327 }
328
329 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
330 label: Some("geom_vertex_buffer"),
331 contents: bytemuck::cast_slice(&verts),
332 usage: wgpu::BufferUsages::VERTEX,
333 });
334
335 let vertex_count = verts.len() as u32;
336
337 let load_op = match clear_color {
338 Some(color) => wgpu::LoadOp::Clear(color),
339 None => wgpu::LoadOp::Load,
340 };
341
342 {
343 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
344 label: Some("geom_render_pass"),
345 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
346 view: target,
347 resolve_target: None,
348 ops: wgpu::Operations {
349 load: load_op,
350 store: wgpu::StoreOp::Store,
351 },
352 })],
353 depth_stencil_attachment: None,
354 timestamp_writes: None,
355 occlusion_query_set: None,
356 });
357
358 pass.set_pipeline(&self.pipeline);
359 pass.set_bind_group(0, camera_bind_group, &[]);
360 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
361 pass.draw(0..vertex_count, 0..1);
362 }
363 }
364
365 pub fn clear(&mut self) {
367 self.vertices.clear();
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn geo_vertex_is_24_bytes() {
377 assert_eq!(std::mem::size_of::<GeoVertex>(), 24);
379 }
380
381 #[test]
382 fn line_quad_geometry_is_correct() {
383 let (x1, y1, x2, y2) = (0.0f32, 0.0, 10.0, 0.0);
385 let thickness = 2.0f32;
386 let dx = x2 - x1;
387 let dy = y2 - y1;
388 let len = (dx * dx + dy * dy).sqrt();
389 let half = thickness * 0.5;
390 let nx = -dy / len * half;
391 let ny = dx / len * half;
392
393 assert!((nx - 0.0).abs() < 1e-6, "nx should be 0 for horizontal line");
395 assert!((ny - 1.0).abs() < 1e-6, "ny should be 1 for horizontal line");
396 }
397
398 #[test]
399 fn diagonal_line_perpendicular() {
400 let (x1, y1, x2, y2) = (0.0f32, 0.0, 10.0, 10.0);
401 let thickness = 2.0f32;
402 let dx = x2 - x1;
403 let dy = y2 - y1;
404 let len = (dx * dx + dy * dy).sqrt();
405 let half = thickness * 0.5;
406 let nx = -dy / len * half;
407 let ny = dx / len * half;
408
409 let perp_len = (nx * nx + ny * ny).sqrt();
411 assert!((perp_len - 1.0).abs() < 1e-6, "perpendicular length should be half-thickness");
412 }
413}