StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Vertex data structures and layouts for wgpu rendering |
| 2 | |
| 3 | use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode}; |
| 4 | |
| 5 | /// Vertex data for UI rendering |
| 6 | #[repr(C)] |
| 7 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] |
| 8 | pub struct Vertex { |
| 9 | pub position: [f32; 2], // Changed from 3D to 2D to match shader |
| 10 | pub color: [f32; 4], |
| 11 | pub uv: [f32; 2], // Renamed from tex_coords to match shader |
| 12 | pub params: [f32; 4], // Added params field to match shader |
| 13 | pub flags: u32, // For different rendering modes (solid, textured, etc.) |
| 14 | } |
| 15 | |
| 16 | impl Vertex { |
| 17 | /// Create a new vertex |
| 18 | pub fn new(position: [f32; 2], color: [f32; 4], uv: [f32; 2]) -> Self { |
| 19 | Self { |
| 20 | position, |
| 21 | color, |
| 22 | uv, |
| 23 | params: [0.0, 0.0, 0.0, 0.0], |
| 24 | flags: 0, |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | /// Create a vertex with solid color (no texture) |
| 29 | pub fn solid(position: [f32; 2], color: [f32; 4]) -> Self { |
| 30 | Self { |
| 31 | position, |
| 32 | color, |
| 33 | uv: [0.0, 0.0], |
| 34 | params: [0.0, 0.0, 0.0, 0.0], |
| 35 | flags: 0, // FLAG_TYPE_SOLID = 0 (matches shader) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /// Create a vertex with texture coordinates |
| 40 | pub fn textured(position: [f32; 2], uv: [f32; 2], color: [f32; 4]) -> Self { |
| 41 | Self { |
| 42 | position, |
| 43 | color, |
| 44 | uv, |
| 45 | params: [0.0, 0.0, 0.0, 0.0], |
| 46 | flags: 1, // FLAG_TYPE_TEXTURED = 1 (matches shader) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | /// Get the vertex buffer layout descriptor |
| 51 | pub fn desc() -> VertexBufferLayout<'static> { |
| 52 | VertexBufferLayout { |
| 53 | array_stride: std::mem::size_of::<Vertex>() as BufferAddress, |
| 54 | step_mode: VertexStepMode::Vertex, |
| 55 | attributes: &[ |
| 56 | // Position |
| 57 | VertexAttribute { |
| 58 | offset: 0, |
| 59 | shader_location: 0, |
| 60 | format: VertexFormat::Float32x2, |
| 61 | }, |
| 62 | // Color |
| 63 | VertexAttribute { |
| 64 | offset: std::mem::size_of::<[f32; 2]>() as BufferAddress, |
| 65 | shader_location: 1, |
| 66 | format: VertexFormat::Float32x4, |
| 67 | }, |
| 68 | // UV coordinates |
| 69 | VertexAttribute { |
| 70 | offset: std::mem::size_of::<[f32; 6]>() as BufferAddress, |
| 71 | shader_location: 2, |
| 72 | format: VertexFormat::Float32x2, |
| 73 | }, |
| 74 | // Params |
| 75 | VertexAttribute { |
| 76 | offset: std::mem::size_of::<[f32; 8]>() as BufferAddress, |
| 77 | shader_location: 3, |
| 78 | format: VertexFormat::Float32x4, |
| 79 | }, |
| 80 | // Flags |
| 81 | VertexAttribute { |
| 82 | offset: std::mem::size_of::<[f32; 12]>() as BufferAddress, |
| 83 | shader_location: 4, |
| 84 | format: VertexFormat::Uint32, |
| 85 | }, |
| 86 | ], |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /// Text vertex for specialized text rendering |
| 92 | #[repr(C)] |
| 93 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] |
| 94 | pub struct TextVertex { |
| 95 | pub position: [f32; 3], |
| 96 | pub tex_coords: [f32; 2], |
| 97 | pub color: [f32; 4], |
| 98 | pub glyph_index: u32, |
| 99 | } |
| 100 | |
| 101 | impl TextVertex { |
| 102 | /// Create a new text vertex |
| 103 | pub fn new( |
| 104 | position: [f32; 3], |
| 105 | tex_coords: [f32; 2], |
| 106 | color: [f32; 4], |
| 107 | glyph_index: u32, |
| 108 | ) -> Self { |
| 109 | Self { |
| 110 | position, |
| 111 | tex_coords, |
| 112 | color, |
| 113 | glyph_index, |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | /// Get the vertex buffer layout descriptor for text |
| 118 | pub fn desc() -> VertexBufferLayout<'static> { |
| 119 | VertexBufferLayout { |
| 120 | array_stride: std::mem::size_of::<TextVertex>() as BufferAddress, |
| 121 | step_mode: VertexStepMode::Vertex, |
| 122 | attributes: &[ |
| 123 | // Position |
| 124 | VertexAttribute { |
| 125 | offset: 0, |
| 126 | shader_location: 0, |
| 127 | format: VertexFormat::Float32x3, |
| 128 | }, |
| 129 | // Texture coordinates |
| 130 | VertexAttribute { |
| 131 | offset: std::mem::size_of::<[f32; 3]>() as BufferAddress, |
| 132 | shader_location: 1, |
| 133 | format: VertexFormat::Float32x2, |
| 134 | }, |
| 135 | // Color |
| 136 | VertexAttribute { |
| 137 | offset: std::mem::size_of::<[f32; 5]>() as BufferAddress, |
| 138 | shader_location: 2, |
| 139 | format: VertexFormat::Float32x4, |
| 140 | }, |
| 141 | // Glyph index |
| 142 | VertexAttribute { |
| 143 | offset: std::mem::size_of::<[f32; 9]>() as BufferAddress, |
| 144 | shader_location: 3, |
| 145 | format: VertexFormat::Uint32, |
| 146 | }, |
| 147 | ], |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | /// Vertex builder for creating common shapes |
| 153 | pub struct VertexBuilder; |
| 154 | |
| 155 | impl VertexBuilder { |
| 156 | /// Create vertices for a rectangle |
| 157 | pub fn rectangle( |
| 158 | x: f32, |
| 159 | y: f32, |
| 160 | width: f32, |
| 161 | height: f32, |
| 162 | color: [f32; 4], |
| 163 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 164 | let vertices = vec![ |
| 165 | Vertex::solid([x, y], color), // Top-left |
| 166 | Vertex::solid([x + width, y], color), // Top-right |
| 167 | Vertex::solid([x + width, y + height], color), // Bottom-right |
| 168 | Vertex::solid([x, y + height], color), // Bottom-left |
| 169 | ]; |
| 170 | |
| 171 | let indices = vec![0, 1, 2, 2, 3, 0]; |
| 172 | |
| 173 | (vertices, indices) |
| 174 | } |
| 175 | |
| 176 | /// Create vertices for a textured rectangle |
| 177 | pub fn textured_rectangle( |
| 178 | x: f32, |
| 179 | y: f32, |
| 180 | width: f32, |
| 181 | height: f32, |
| 182 | color: [f32; 4], |
| 183 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 184 | let vertices = vec![ |
| 185 | Vertex::textured([x, y], [0.0, 0.0], color), // Top-left |
| 186 | Vertex::textured([x + width, y], [1.0, 0.0], color), // Top-right |
| 187 | Vertex::textured([x + width, y + height], [1.0, 1.0], color), // Bottom-right |
| 188 | Vertex::textured([x, y + height], [0.0, 1.0], color), // Bottom-left |
| 189 | ]; |
| 190 | |
| 191 | let indices = vec![0, 1, 2, 2, 3, 0]; |
| 192 | |
| 193 | (vertices, indices) |
| 194 | } |
| 195 | |
| 196 | /// Create vertices for a circle (approximated with triangles) |
| 197 | pub fn circle( |
| 198 | center_x: f32, |
| 199 | center_y: f32, |
| 200 | radius: f32, |
| 201 | color: [f32; 4], |
| 202 | segments: u32, |
| 203 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 204 | let mut vertices = Vec::with_capacity((segments + 1) as usize); |
| 205 | let mut indices = Vec::with_capacity((segments * 3) as usize); |
| 206 | |
| 207 | // Center vertex |
| 208 | vertices.push(Vertex::solid([center_x, center_y], color)); |
| 209 | |
| 210 | // Create vertices around the circle |
| 211 | for i in 0..segments { |
| 212 | let angle = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32); |
| 213 | let x = center_x + radius * angle.cos(); |
| 214 | let y = center_y + radius * angle.sin(); |
| 215 | vertices.push(Vertex::solid([x, y], color)); |
| 216 | } |
| 217 | |
| 218 | // Create triangles |
| 219 | for i in 0..segments { |
| 220 | let next = if i == segments - 1 { 1 } else { i + 2 }; |
| 221 | indices.extend_from_slice(&[0, (i + 1) as u16, next as u16]); |
| 222 | } |
| 223 | |
| 224 | (vertices, indices) |
| 225 | } |
| 226 | |
| 227 | /// Create vertices for a rounded rectangle using SDF |
| 228 | pub fn rounded_rectangle( |
| 229 | x: f32, |
| 230 | y: f32, |
| 231 | width: f32, |
| 232 | height: f32, |
| 233 | radius: f32, |
| 234 | color: [f32; 4], |
| 235 | _corner_segments: u32, // Unused for SDF |
| 236 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 237 | let mut vertices = Vec::with_capacity(4); |
| 238 | let indices = vec![0, 1, 2, 2, 3, 0]; |
| 239 | |
| 240 | // Params: width, height, radius, unused |
| 241 | let params = [width, height, radius, 0.0]; |
| 242 | // Flag: 3 = FLAG_TYPE_ROUNDED_RECT |
| 243 | let flags = 3; |
| 244 | |
| 245 | vertices.push(Vertex { |
| 246 | position: [x, y], |
| 247 | color, |
| 248 | uv: [0.0, 0.0], |
| 249 | params, |
| 250 | flags, |
| 251 | }); |
| 252 | vertices.push(Vertex { |
| 253 | position: [x + width, y], |
| 254 | color, |
| 255 | uv: [1.0, 0.0], |
| 256 | params, |
| 257 | flags, |
| 258 | }); |
| 259 | vertices.push(Vertex { |
| 260 | position: [x + width, y + height], |
| 261 | color, |
| 262 | uv: [1.0, 1.0], |
| 263 | params, |
| 264 | flags, |
| 265 | }); |
| 266 | vertices.push(Vertex { |
| 267 | position: [x, y + height], |
| 268 | color, |
| 269 | uv: [0.0, 1.0], |
| 270 | params, |
| 271 | flags, |
| 272 | }); |
| 273 | |
| 274 | (vertices, indices) |
| 275 | } |
| 276 | |
| 277 | /// Create vertices for a rounded rectangle outline (border) |
| 278 | pub fn rounded_rectangle_outline( |
| 279 | x: f32, |
| 280 | y: f32, |
| 281 | width: f32, |
| 282 | height: f32, |
| 283 | radius: f32, |
| 284 | color: [f32; 4], |
| 285 | thickness: f32, |
| 286 | corner_segments: u32, |
| 287 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 288 | let mut vertices = Vec::new(); |
| 289 | let mut indices = Vec::new(); |
| 290 | |
| 291 | // Create the four border lines |
| 292 | let half_thickness = thickness / 2.0; |
| 293 | |
| 294 | // Top line |
| 295 | let (top_verts, top_indices) = Self::line( |
| 296 | x + radius, |
| 297 | y - half_thickness, |
| 298 | x + width - radius, |
| 299 | y - half_thickness, |
| 300 | thickness, |
| 301 | color, |
| 302 | ); |
| 303 | vertices.extend(top_verts); |
| 304 | indices.extend(top_indices); |
| 305 | |
| 306 | // Right line |
| 307 | let offset = vertices.len() as u16; |
| 308 | let (right_verts, right_indices) = Self::line( |
| 309 | x + width + half_thickness, |
| 310 | y + radius, |
| 311 | x + width + half_thickness, |
| 312 | y + height - radius, |
| 313 | thickness, |
| 314 | color, |
| 315 | ); |
| 316 | vertices.extend(right_verts); |
| 317 | indices.extend(right_indices.iter().map(|&i| i + offset)); |
| 318 | |
| 319 | // Bottom line |
| 320 | let offset = vertices.len() as u16; |
| 321 | let (bottom_verts, bottom_indices) = Self::line( |
| 322 | x + width - radius, |
| 323 | y + height + half_thickness, |
| 324 | x + radius, |
| 325 | y + height + half_thickness, |
| 326 | thickness, |
| 327 | color, |
| 328 | ); |
| 329 | vertices.extend(bottom_verts); |
| 330 | indices.extend(bottom_indices.iter().map(|&i| i + offset)); |
| 331 | |
| 332 | // Left line |
| 333 | let offset = vertices.len() as u16; |
| 334 | let (left_verts, left_indices) = Self::line( |
| 335 | x - half_thickness, |
| 336 | y + height - radius, |
| 337 | x - half_thickness, |
| 338 | y + radius, |
| 339 | thickness, |
| 340 | color, |
| 341 | ); |
| 342 | vertices.extend(left_verts); |
| 343 | indices.extend(left_indices.iter().map(|&i| i + offset)); |
| 344 | |
| 345 | // Add rounded corners (outline arcs) |
| 346 | let corners = [ |
| 347 | (x + radius, y + radius), // Top-left |
| 348 | (x + width - radius, y + radius), // Top-right |
| 349 | (x + width - radius, y + height - radius), // Bottom-right |
| 350 | (x + radius, y + height - radius), // Bottom-left |
| 351 | ]; |
| 352 | |
| 353 | for (i, &(cx, cy)) in corners.iter().enumerate() { |
| 354 | let start_angle = (i as f32) * std::f32::consts::PI / 2.0 + std::f32::consts::PI; |
| 355 | |
| 356 | // Create arc outline using multiple line segments |
| 357 | for j in 0..corner_segments { |
| 358 | let angle1 = start_angle |
| 359 | + (j as f32) * (std::f32::consts::PI / 2.0) / (corner_segments as f32); |
| 360 | let angle2 = start_angle |
| 361 | + ((j + 1) as f32) * (std::f32::consts::PI / 2.0) / (corner_segments as f32); |
| 362 | |
| 363 | let x1 = cx + radius * angle1.cos(); |
| 364 | let y1 = cy + radius * angle1.sin(); |
| 365 | let x2 = cx + radius * angle2.cos(); |
| 366 | let y2 = cy + radius * angle2.sin(); |
| 367 | |
| 368 | let offset = vertices.len() as u16; |
| 369 | let (arc_verts, arc_indices) = Self::line(x1, y1, x2, y2, thickness, color); |
| 370 | vertices.extend(arc_verts); |
| 371 | indices.extend(arc_indices.iter().map(|&i| i + offset)); |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | (vertices, indices) |
| 376 | } |
| 377 | |
| 378 | /// Create vertices for a circle sector |
| 379 | fn circle_sector( |
| 380 | center_x: f32, |
| 381 | center_y: f32, |
| 382 | radius: f32, |
| 383 | color: [f32; 4], |
| 384 | segments: u32, |
| 385 | start_angle: f32, |
| 386 | angle_span: f32, |
| 387 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 388 | let mut vertices = Vec::with_capacity((segments + 1) as usize); |
| 389 | let mut indices = Vec::with_capacity((segments * 3) as usize); |
| 390 | |
| 391 | // Center vertex |
| 392 | vertices.push(Vertex::solid([center_x, center_y], color)); |
| 393 | |
| 394 | // Create vertices around the sector |
| 395 | for i in 0..=segments { |
| 396 | let angle = start_angle + (i as f32) * angle_span / (segments as f32); |
| 397 | let x = center_x + radius * angle.cos(); |
| 398 | let y = center_y + radius * angle.sin(); |
| 399 | vertices.push(Vertex::solid([x, y], color)); |
| 400 | } |
| 401 | |
| 402 | // Create triangles |
| 403 | for i in 0..segments { |
| 404 | indices.extend_from_slice(&[0, (i + 1) as u16, (i + 2) as u16]); |
| 405 | } |
| 406 | |
| 407 | (vertices, indices) |
| 408 | } |
| 409 | |
| 410 | /// Create vertices for a line with thickness |
| 411 | pub fn line( |
| 412 | start_x: f32, |
| 413 | start_y: f32, |
| 414 | end_x: f32, |
| 415 | end_y: f32, |
| 416 | thickness: f32, |
| 417 | color: [f32; 4], |
| 418 | ) -> (Vec<Vertex>, Vec<u16>) { |
| 419 | let dx = end_x - start_x; |
| 420 | let dy = end_y - start_y; |
| 421 | let length = (dx * dx + dy * dy).sqrt(); |
| 422 | |
| 423 | if length == 0.0 { |
| 424 | return (Vec::new(), Vec::new()); |
| 425 | } |
| 426 | |
| 427 | // Normalize and get perpendicular vector |
| 428 | let nx = -dy / length; |
| 429 | let ny = dx / length; |
| 430 | |
| 431 | let half_thickness = thickness * 0.5; |
| 432 | |
| 433 | let vertices = vec![ |
| 434 | Vertex::solid( |
| 435 | [start_x + nx * half_thickness, start_y + ny * half_thickness], |
| 436 | color, |
| 437 | ), |
| 438 | Vertex::solid( |
| 439 | [start_x - nx * half_thickness, start_y - ny * half_thickness], |
| 440 | color, |
| 441 | ), |
| 442 | Vertex::solid( |
| 443 | [end_x - nx * half_thickness, end_y - ny * half_thickness], |
| 444 | color, |
| 445 | ), |
| 446 | Vertex::solid( |
| 447 | [end_x + nx * half_thickness, end_y + ny * half_thickness], |
| 448 | color, |
| 449 | ), |
| 450 | ]; |
| 451 | |
| 452 | let indices = vec![0, 1, 2, 2, 3, 0]; |
| 453 | |
| 454 | (vertices, indices) |
| 455 | } |
| 456 | } |
| 457 | |
| 458 | #[cfg(test)] |
| 459 | mod tests { |
| 460 | use super::*; |
| 461 | |
| 462 | #[test] |
| 463 | fn test_vertex_creation() { |
| 464 | let vertex = Vertex::new([1.0, 2.0], [1.0, 0.0, 0.0, 1.0], [0.5, 0.5]); |
| 465 | assert_eq!(vertex.position, [1.0, 2.0]); |
| 466 | assert_eq!(vertex.color, [1.0, 0.0, 0.0, 1.0]); |
| 467 | assert_eq!(vertex.uv, [0.5, 0.5]); |
| 468 | } |
| 469 | |
| 470 | #[test] |
| 471 | fn test_vertex_solid() { |
| 472 | let vertex = Vertex::solid([0.0, 0.0], [1.0, 1.0, 1.0, 1.0]); |
| 473 | assert_eq!(vertex.flags, 0); |
| 474 | assert_eq!(vertex.uv, [0.0, 0.0]); |
| 475 | } |
| 476 | |
| 477 | #[test] |
| 478 | fn test_vertex_textured() { |
| 479 | let vertex = Vertex::textured([0.0, 0.0], [1.0, 1.0], [1.0, 1.0, 1.0, 1.0]); |
| 480 | assert_eq!(vertex.flags, 1); |
| 481 | assert_eq!(vertex.uv, [1.0, 1.0]); |
| 482 | } |
| 483 | |
| 484 | #[test] |
| 485 | fn test_rectangle_builder() { |
| 486 | let (vertices, indices) = |
| 487 | VertexBuilder::rectangle(0.0, 0.0, 100.0, 50.0, [1.0, 0.0, 0.0, 1.0]); |
| 488 | assert_eq!(vertices.len(), 4); |
| 489 | assert_eq!(indices.len(), 6); |
| 490 | assert_eq!(vertices[0].position, [0.0, 0.0]); |
| 491 | assert_eq!(vertices[2].position, [100.0, 50.0]); |
| 492 | } |
| 493 | |
| 494 | #[test] |
| 495 | fn test_circle_builder() { |
| 496 | let (vertices, indices) = VertexBuilder::circle(50.0, 50.0, 25.0, [0.0, 1.0, 0.0, 1.0], 8); |
| 497 | assert_eq!(vertices.len(), 9); // Center + 8 segments |
| 498 | assert_eq!(indices.len(), 24); // 8 triangles * 3 indices |
| 499 | } |
| 500 | } |
| 501 |