Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-renderer/src/gpu/drawing.rs
StratoSDK / crates / strato-renderer / src / gpu / drawing.rs
1//! Drawing system - integrates all GPU components
2//!
3//! BLOCCO 7: Drawing System
4//! Final integration: converts RenderBatch to GPU draw calls
5 
6use super::{
7 buffer_mgr::{BufferManager, SimpleVertex},
8 device::DeviceManager,
9 pipeline_mgr::PipelineManager,
10 render_pass_mgr::RenderPassManager,
11 shader_mgr::ShaderManager,
12 surface::SurfaceManager,
13 texture_mgr::TextureManager,
14};
15use crate::batch::RenderBatch;
16use crate::vertex::VertexBuilder;
17use std::sync::Arc;
18use wgpu::{CommandEncoderDescriptor, IndexFormat};
19use winit::window::Window;
20 
21/// Complete drawing system
22pub struct DrawingSystem {
23 device_mgr: DeviceManager,
24 surface_mgr: SurfaceManager,
25 shader_mgr: ShaderManager,
26 buffer_mgr: BufferManager,
27 texture_mgr: TextureManager,
28 pipeline_mgr: PipelineManager,
29 render_pass_mgr: RenderPassManager,
30 scale_factor: f32,
31}
32 
33impl DrawingSystem {
34 /// Create new drawing system
35 pub async fn new(window: Arc<Window>) -> anyhow::Result<Self> {
36 println!("=== DRAWING SYSTEM INITIALIZATION ===");
37 
38 // BLOCCO 1: Device Setup
39 let device_mgr = DeviceManager::new(wgpu::Backends::all()).await?;
40 println!("✅ DeviceManager initialized");
41 
42 // BLOCCO 2: Surface Configuration
43 let target = unsafe { wgpu::SurfaceTargetUnsafe::from_window(&*window)? };
44 let surface = unsafe { device_mgr.instance().create_surface_unsafe(target)? };
45 let size = window.inner_size();
46 
47 let surface_mgr = SurfaceManager::new(
48 surface,
49 device_mgr.device(),
50 device_mgr.adapter(),
51 size.width,
52 size.height,
53 )?;
54 println!("✅ SurfaceManager initialized");
55 
56 // BLOCCO 3: Shader Compilation
57 let shader_mgr = ShaderManager::from_wgsl(
58 device_mgr.device(),
59 include_str!("../shaders/simple.wgsl"),
60 Some("Simple Shader"),
61 )?;
62 println!("✅ ShaderManager initialized");
63 
64 // BLOCCO 4: Buffer Management
65 let buffer_mgr = BufferManager::new(device_mgr.device());
66 println!("✅ BufferManager initialized");
67 
68 // BLOCCO 8: Texture Management
69 let texture_mgr = TextureManager::new_with_font(device_mgr.device(), device_mgr.queue());
70 println!("✅ TextureManager initialized");
71 
72 // BLOCCO 5: Pipeline Creation
73 let pipeline_mgr = PipelineManager::new(
74 device_mgr.device(),
75 &shader_mgr,
76 &buffer_mgr,
77 &texture_mgr,
78 surface_mgr.format(),
79 )?;
80 println!("✅ PipelineManager initialized");
81 
82 // BLOCCO 6: Render Pass
83 let render_pass_mgr = RenderPassManager::new();
84 println!("✅ RenderPassManager initialized");
85 
86 println!("====================================");
87 
88 Ok(Self {
89 device_mgr,
90 surface_mgr,
91 shader_mgr,
92 buffer_mgr,
93 texture_mgr,
94 pipeline_mgr,
95 render_pass_mgr,
96 scale_factor: 1.0,
97 })
98 }
99 
100 /// Set the DPI scale factor
101 pub fn set_scale_factor(&mut self, scale_factor: f32) {
102 self.scale_factor = scale_factor;
103 }
104 
105 /// Render a batch
106 pub fn render(&mut self, batch: &RenderBatch) -> anyhow::Result<()> {
107 // 1. Process batch commands to generate vertices (including text)
108 let mut vertices: Vec<SimpleVertex> = Vec::new();
109 let mut indices: Vec<u32> = Vec::new();
110 let mut vertex_count = 0;
111 
112 // Clipping state
113 struct GPUDrawBatch {
114 index_start: u32,
115 index_count: u32,
116 scissor: Option<[u32; 4]>,
117 }
118 let mut batches: Vec<GPUDrawBatch> = Vec::new();
119 let mut current_index_start = 0;
120 let mut current_index_count = 0;
121 let mut scissor_stack: Vec<[u32; 4]> = Vec::new();
122 
123 let get_current_scissor =
124 |stack: &[[u32; 4]]| -> Option<[u32; 4]> { stack.last().cloned() };
125 
126 // Note: We ignore batch.vertices here because we regenerate everything from commands
127 // to ensure correct Z-ordering and support interleaved clipping.
128 
129 for command in &batch.commands {
130 match command {
131 crate::batch::DrawCommand::PushClip(rect) => {
132 // Finish current batch if needed
133 if current_index_count > 0 {
134 batches.push(GPUDrawBatch {
135 index_start: current_index_start,
136 index_count: current_index_count,
137 scissor: get_current_scissor(&scissor_stack),
138 });
139 current_index_start += current_index_count;
140 current_index_count = 0;
141 }
142 
143 // Calculate new scissor rect
144 let scale = self.scale_factor;
145 let x = (rect.x as f32 * scale).round() as i32;
146 let y = (rect.y as f32 * scale).round() as i32;
147 let w = (rect.width as f32 * scale).round() as i32;
148 let h = (rect.height as f32 * scale).round() as i32;
149 
150 let surface_w = self.surface_mgr.width() as i32;
151 let surface_h = self.surface_mgr.height() as i32;
152 
153 // Intersect with surface bounds
154 let min_x = x.max(0);
155 let min_y = y.max(0);
156 let max_x = (x + w).min(surface_w).max(min_x);
157 let max_y = (y + h).min(surface_h).max(min_y);
158 
159 let mut new_rect = [
160 min_x as u32,
161 min_y as u32,
162 (max_x - min_x) as u32,
163 (max_y - min_y) as u32,
164 ];
165 
166 // Intersect with current scissor
167 if let Some(parent) = scissor_stack.last() {
168 let px = parent[0];
169 let py = parent[1];
170 let pw = parent[2];
171 let ph = parent[3];
172 
173 let ix = new_rect[0].max(px);
174 let iy = new_rect[1].max(py);
175 let iw = (new_rect[0] + new_rect[2]).min(px + pw).saturating_sub(ix);
176 let ih = (new_rect[1] + new_rect[3]).min(py + ph).saturating_sub(iy);
177 
178 new_rect = [ix, iy, iw, ih];
179 }
180 
181 scissor_stack.push(new_rect);
182 }
183 crate::batch::DrawCommand::PopClip => {
184 // Finish current batch if needed
185 if current_index_count > 0 {
186 batches.push(GPUDrawBatch {
187 index_start: current_index_start,
188 index_count: current_index_count,
189 scissor: get_current_scissor(&scissor_stack),
190 });
191 current_index_start += current_index_count;
192 current_index_count = 0;
193 }
194 scissor_stack.pop();
195 }
196 crate::batch::DrawCommand::RoundedRect {
197 rect,
198 color,
199 radius,
200 transform,
201 } => {
202 let color_arr = [color.r, color.g, color.b, color.a];
203 let (v_list, i_list) = VertexBuilder::rounded_rectangle(
204 rect.x,
205 rect.y,
206 rect.width,
207 rect.height,
208 *radius,
209 color_arr,
210 8,
211 );
212 
213 let added_count = v_list.len() as u32;
214 let index_count = i_list.len() as u32;
215 
216 for v in v_list {
217 let mut sv = SimpleVertex::from(&v);
218 // Apply transform
219 let p = strato_core::types::Point::new(sv.position[0], sv.position[1]);
220 let transformed = transform.transform_point(p);
221 sv.position = [transformed.x, transformed.y];
222 vertices.push(sv);
223 }
224 
225 for i in i_list {
226 indices.push((i as u32) + vertex_count);
227 }
228 vertex_count += added_count;
229 current_index_count += index_count;
230 }
231 crate::batch::DrawCommand::Rect {
232 rect,
233 color,
234 transform,
235 ..
236 } => {
237 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
238 
239 // Apply transform using strato_core::Transform method
240 let apply_transform = |p: [f32; 2]| -> [f32; 2] {
241 let point = strato_core::types::Point::new(p[0], p[1]);
242 let transformed = transform.transform_point(point);
243 [transformed.x, transformed.y]
244 };
245 
246 let p0 = apply_transform([x, y]);
247 let p1 = apply_transform([x + w, y]);
248 let p2 = apply_transform([x + w, y + h]);
249 let p3 = apply_transform([x, y + h]);
250 
251 let color_arr = [color.r, color.g, color.b, color.a];
252 
253 // Solid color vertices (uv = 0,0)
254 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
255 p0, color_arr,
256 )));
257 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
258 p1, color_arr,
259 )));
260 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
261 p2, color_arr,
262 )));
263 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
264 p3, color_arr,
265 )));
266 
267 indices.push(vertex_count);
268 indices.push(vertex_count + 1);
269 indices.push(vertex_count + 2);
270 indices.push(vertex_count);
271 indices.push(vertex_count + 2);
272 indices.push(vertex_count + 3);
273 
274 vertex_count += 4;
275 current_index_count += 6;
276 }
277 crate::batch::DrawCommand::Text {
278 text,
279 position,
280 color,
281 font_size,
282 letter_spacing,
283 align,
284 } => {
285 let (mut x, y) = *position;
286 let color_arr = [color.r, color.g, color.b, color.a];
287 let font_size_val = *font_size;
288 let spacing_val = *letter_spacing;
289 
290 // Use scale factor for high-resolution text rasterization
291 let scale = self.scale_factor;
292 let physical_font_size = (font_size_val * scale).round() as u32;
293 
294 // Handle alignment
295 if *align != strato_core::text::TextAlign::Left {
296 let mut width = 0.0;
297 for ch in text.chars() {
298 if let Some(glyph) = self.texture_mgr.get_or_cache_glyph(
299 self.device_mgr.queue(),
300 ch,
301 physical_font_size,
302 ) {
303 // Scale metrics back to logical coordinates for layout
304 let advance = glyph.metrics.advance / scale;
305 width += advance + spacing_val;
306 } else if ch == ' ' {
307 width += font_size_val * 0.3 + spacing_val;
308 }
309 }
310 
311 match align {
312 strato_core::text::TextAlign::Center => x -= width / 2.0,
313 strato_core::text::TextAlign::Right => x -= width,
314 _ => {} // Justify not implemented yet
315 }
316 }
317 
318 let ascent = if let Some(metrics) =
319 self.texture_mgr.get_line_metrics(physical_font_size as f32)
320 {
321 metrics.ascent / scale
322 } else {
323 font_size_val * 0.8 // Fallback approximation
324 };
325 
326 let baseline = y + ascent;
327 
328 for ch in text.chars() {
329 if let Some(glyph) = self.texture_mgr.get_or_cache_glyph(
330 self.device_mgr.queue(),
331 ch,
332 physical_font_size,
333 ) {
334 // Scale metrics back to logical coordinates for rendering
335 let bearing_x = glyph.metrics.bearing_x as f32 / scale;
336 let bearing_y = glyph.metrics.bearing_y as f32 / scale;
337 let w = glyph.metrics.width as f32 / scale;
338 let h = glyph.metrics.height as f32 / scale;
339 let advance = glyph.metrics.advance / scale;
340 
341 let glyph_x = (x + bearing_x).round();
342 let glyph_y = (baseline - bearing_y).round();
343 
344 let (u0, v0, u1, v1) = glyph.uv_rect;
345 
346 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
347 [glyph_x, glyph_y],
348 [u0, v0],
349 color_arr,
350 )));
351 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
352 [glyph_x + w, glyph_y],
353 [u1, v0],
354 color_arr,
355 )));
356 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
357 [glyph_x + w, glyph_y + h],
358 [u1, v1],
359 color_arr,
360 )));
361 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
362 [glyph_x, glyph_y + h],
363 [u0, v1],
364 color_arr,
365 )));
366 
367 indices.push(vertex_count);
368 indices.push(vertex_count + 1);
369 indices.push(vertex_count + 2);
370 indices.push(vertex_count);
371 indices.push(vertex_count + 2);
372 indices.push(vertex_count + 3);
373 
374 vertex_count += 4;
375 current_index_count += 6;
376 
377 x += advance + spacing_val;
378 } else {
379 if ch == ' ' {
380 x += font_size_val * 0.3 + spacing_val;
381 }
382 }
383 }
384 }
385 crate::batch::DrawCommand::Image {
386 id,
387 data,
388 width,
389 height,
390 rect,
391 color,
392 } => {
393 if let Some(image) = self.texture_mgr.get_or_upload_image(
394 self.device_mgr.queue(),
395 *id,
396 data,
397 *width,
398 *height,
399 ) {
400 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
401 let (u0, v0, u1, v1) = image.uv_rect;
402 let color_arr = [color.r, color.g, color.b, color.a];
403 
404 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
405 [x, y],
406 [u0, v0],
407 color_arr,
408 )));
409 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
410 [x + w, y],
411 [u1, v0],
412 color_arr,
413 )));
414 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
415 [x + w, y + h],
416 [u1, v1],
417 color_arr,
418 )));
419 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
420 [x, y + h],
421 [u0, v1],
422 color_arr,
423 )));
424 
425 indices.push(vertex_count);
426 indices.push(vertex_count + 1);
427 indices.push(vertex_count + 2);
428 indices.push(vertex_count);
429 indices.push(vertex_count + 2);
430 indices.push(vertex_count + 3);
431 
432 vertex_count += 4;
433 current_index_count += 6;
434 }
435 }
436 crate::batch::DrawCommand::TexturedQuad {
437 rect,
438 texture_id: _,
439 uv_rect,
440 color,
441 transform,
442 ..
443 } => {
444 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
445 let (u, v, uw, vh) = (uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height);
446 let color_arr = [color.r, color.g, color.b, color.a];
447 
448 let apply_transform = |p: [f32; 2]| -> [f32; 2] {
449 let point = strato_core::types::Point::new(p[0], p[1]);
450 let transformed = transform.transform_point(point);
451 [transformed.x, transformed.y]
452 };
453 
454 let p0 = apply_transform([x, y]);
455 let p1 = apply_transform([x + w, y]);
456 let p2 = apply_transform([x + w, y + h]);
457 let p3 = apply_transform([x, y + h]);
458 
459 let uv0 = [u, v];
460 let uv1 = [u + uw, v];
461 let uv2 = [u + uw, v + vh];
462 let uv3 = [u, v + vh];
463 
464 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
465 p0, uv0, color_arr,
466 )));
467 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
468 p1, uv1, color_arr,
469 )));
470 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
471 p2, uv2, color_arr,
472 )));
473 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(
474 p3, uv3, color_arr,
475 )));
476 
477 indices.push(vertex_count);
478 indices.push(vertex_count + 1);
479 indices.push(vertex_count + 2);
480 indices.push(vertex_count);
481 indices.push(vertex_count + 2);
482 indices.push(vertex_count + 3);
483 
484 vertex_count += 4;
485 current_index_count += 6;
486 }
487 crate::batch::DrawCommand::Circle {
488 center,
489 radius,
490 color,
491 segments,
492 ..
493 } => {
494 let (cx, cy) = *center;
495 let radius = *radius;
496 let color_arr = [color.r, color.g, color.b, color.a];
497 let segments = *segments;
498 
499 // Center vertex
500 vertices.push(SimpleVertex::from(&crate::vertex::Vertex {
501 position: [cx, cy],
502 uv: [0.5, 0.5],
503 color: color_arr,
504 params: [0.0, 0.0, 0.0, 0.0],
505 flags: 0,
506 }));
507 
508 let center_index = vertex_count;
509 vertex_count += 1;
510 
511 for i in 0..=segments {
512 let angle = (i as f32 / segments as f32) * 2.0 * std::f32::consts::PI;
513 let x = cx + radius * angle.cos();
514 let y = cy + radius * angle.sin();
515 
516 vertices.push(SimpleVertex::from(&crate::vertex::Vertex {
517 position: [x, y],
518 uv: [0.5 + 0.5 * angle.cos(), 0.5 + 0.5 * angle.sin()],
519 color: color_arr,
520 params: [0.0, 0.0, 0.0, 0.0],
521 flags: 0,
522 }));
523 
524 if i > 0 {
525 indices.push(center_index);
526 indices.push(vertex_count - 1);
527 indices.push(vertex_count);
528 current_index_count += 3;
529 }
530 
531 vertex_count += 1;
532 }
533 }
534 crate::batch::DrawCommand::Line {
535 start,
536 end,
537 color,
538 thickness,
539 ..
540 } => {
541 let (x1, y1) = *start;
542 let (x2, y2) = *end;
543 let thickness = *thickness;
544 let color_arr = [color.r, color.g, color.b, color.a];
545 
546 let dx = x2 - x1;
547 let dy = y2 - y1;
548 let length = (dx * dx + dy * dy).sqrt();
549 
550 if length > 0.0 {
551 let nx = -dy / length * thickness * 0.5;
552 let ny = dx / length * thickness * 0.5;
553 
554 let p0 = [x1 + nx, y1 + ny];
555 let p1 = [x2 + nx, y2 + ny];
556 let p2 = [x2 - nx, y2 - ny];
557 let p3 = [x1 - nx, y1 - ny];
558 
559 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
560 p0, color_arr,
561 )));
562 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
563 p1, color_arr,
564 )));
565 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
566 p2, color_arr,
567 )));
568 vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(
569 p3, color_arr,
570 )));
571 
572 indices.push(vertex_count);
573 indices.push(vertex_count + 1);
574 indices.push(vertex_count + 2);
575 indices.push(vertex_count);
576 indices.push(vertex_count + 2);
577 indices.push(vertex_count + 3);
578 
579 vertex_count += 4;
580 current_index_count += 6;
581 }
582 }
583 }
584 }
585 
586 // Push final batch
587 if current_index_count > 0 {
588 batches.push(GPUDrawBatch {
589 index_start: current_index_start,
590 index_count: current_index_count,
591 scissor: get_current_scissor(&scissor_stack),
592 });
593 }
594 
595 // 3. Upload vertices and indices to GPU
596 self.buffer_mgr.upload_vertices(
597 self.device_mgr.device(),
598 self.device_mgr.queue(),
599 &vertices,
600 );
601 self.buffer_mgr
602 .upload_indices(self.device_mgr.device(), self.device_mgr.queue(), &indices);
603 
604 // 4. Upload projection matrix (orthographic for 2D)
605 // Use logical size for projection to handle DPI scaling correctly
606 let width = self.surface_mgr.width() as f32;
607 let height = self.surface_mgr.height() as f32;
608 
609 // Adjust projection for DPI scale factor
610 // If scale_factor is 2.0 (Retina), physical width is 2x logical width.
611 // We want to use logical coordinates (e.g. 0..400) which map to physical pixels (0..800).
612 // So we project 0..width/scale to -1..1.
613 let logical_width = width / self.scale_factor;
614 let logical_height = height / self.scale_factor;
615 
616 let projection = create_orthographic_projection(logical_width, logical_height);
617 self.buffer_mgr
618 .upload_projection(self.device_mgr.queue(), &projection);
619 
620 // 5. Get surface texture
621 let surface_texture = self.surface_mgr.get_current_texture()?;
622 let view = surface_texture
623 .texture
624 .create_view(&wgpu::TextureViewDescriptor::default());
625 
626 // 6. Create command encoder
627 let mut encoder =
628 self.device_mgr
629 .device()
630 .create_command_encoder(&CommandEncoderDescriptor {
631 label: Some("Render Encoder"),
632 });
633 
634 // 7. Begin render pass
635 {
636 let mut render_pass = self.render_pass_mgr.begin(&mut encoder, &view);
637 
638 // 8. Set pipeline and bind groups
639 render_pass.set_pipeline(self.pipeline_mgr.pipeline());
640 render_pass.set_bind_group(0, self.pipeline_mgr.bind_group(), &[]);
641 
642 // 9. Set vertex/index buffers
643 render_pass.set_vertex_buffer(0, self.buffer_mgr.vertex_buffer().slice(..));
644 render_pass.set_index_buffer(
645 self.buffer_mgr.index_buffer().slice(..),
646 IndexFormat::Uint32,
647 );
648 
649 // 10. Draw indexed
650 for batch in batches {
651 if batch.index_count == 0 {
652 continue;
653 }
654 
655 // Apply scissor
656 if let Some(scissor) = batch.scissor {
657 if scissor[2] == 0 || scissor[3] == 0 {
658 continue;
659 }
660 render_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]);
661 } else {
662 render_pass.set_scissor_rect(
663 0,
664 0,
665 self.surface_mgr.width(),
666 self.surface_mgr.height(),
667 );
668 }
669 
670 render_pass.draw_indexed(
671 batch.index_start..batch.index_start + batch.index_count,
672 0,
673 0..1,
674 );
675 }
676 }
677 
678 // 11. Submit command buffer
679 self.device_mgr
680 .queue()
681 .submit(std::iter::once(encoder.finish()));
682 
683 // 12. Present surface
684 surface_texture.present();
685 
686 Ok(())
687 }
688 
689 /// Resize surface
690 pub fn resize(&mut self, width: u32, height: u32) -> anyhow::Result<()> {
691 self.surface_mgr
692 .resize(width, height, self.device_mgr.device())?;
693 
694 // Update projection matrix
695 // Use logical size for projection to match render() behavior
696 let logical_width = (width as f32) / self.scale_factor;
697 let logical_height = (height as f32) / self.scale_factor;
698 
699 let projection = create_orthographic_projection(logical_width, logical_height);
700 self.buffer_mgr
701 .upload_projection(self.device_mgr.queue(), &projection);
702 
703 Ok(())
704 }
705}
706 
707/// Create orthographic projection matrix for 2D rendering
708fn create_orthographic_projection(width: f32, height: f32) -> [[f32; 4]; 4] {
709 // NDC: x: -1 to 1, y: -1 to 1
710 // Screen: x: 0 to width, y: 0 to height
711 let left = 0.0;
712 let right = width;
713 let bottom = height;
714 let top = 0.0;
715 
716 [
717 [2.0 / (right - left), 0.0, 0.0, 0.0],
718 [0.0, 2.0 / (top - bottom), 0.0, 0.0],
719 [0.0, 0.0, 1.0, 0.0],
720 [
721 -(right + left) / (right - left),
722 -(top + bottom) / (top - bottom),
723 0.0,
724 1.0,
725 ],
726 ]
727}
728 
729/// Convert existing Vertex to SimpleVertex
730impl From<&crate::vertex::Vertex> for SimpleVertex {
731 fn from(v: &crate::vertex::Vertex) -> Self {
732 Self {
733 position: v.position,
734 color: v.color,
735 uv: v.uv, // Use UV from existing Vertex struct
736 params: v.params,
737 flags: v.flags,
738 }
739 }
740}
741 
742#[cfg(test)]
743mod tests {
744 use super::*;
745 
746 #[test]
747 fn test_vertex_conversion() {
748 let vertex = crate::vertex::Vertex::solid([100.0, 200.0], [1.0, 0.0, 0.0, 1.0]);
749 let simple: SimpleVertex = (&vertex).into();
750 
751 assert_eq!(simple.position, [100.0, 200.0]);
752 assert_eq!(simple.color, [1.0, 0.0, 0.0, 1.0]);
753 assert_eq!(simple.uv, vertex.uv);
754 }
755 
756 #[test]
757 fn test_orthographic_projection() {
758 let proj = create_orthographic_projection(800.0, 600.0);
759 
760 // Top-left corner (0, 0) should map to NDC (-1, 1)
761 // Bottom-right (800, 600) should map to NDC (1, -1)
762 
763 // Check matrix is not identity
764 assert_ne!(proj[0][0], 1.0);
765 assert_ne!(proj[1][1], 1.0);
766 }
767}
768