1use crate::renderer::GpuRenderer;
3
4pub mod frame;
5pub mod shapes;
6pub mod text;
7
8use crate::renderer::material_id;
9use crate::types::*;
10use crate::vertex::*;
11use cvkg_core::LAYOUT_DIRTY;
12use cvkg_core::{ColorTheme, Mesh, Rect, RenderStateSnapshot, Renderer};
13use lyon::math::point;
14use lyon::tessellation::{BuffersBuilder, StrokeOptions, StrokeTessellator, VertexBuffers};
15use std::hash::{Hash, Hasher};
16use std::sync::atomic::Ordering;
17
18impl cvkg_core::ElapsedTime for GpuRenderer {
19 fn delta_time(&self) -> f32 {
20 self.current_scene.delta_time
21 }
22
23 fn elapsed_time(&self) -> f32 {
24 self.start_time.elapsed().as_secs_f32()
25 }
26}
27
28impl cvkg_core::RendererErrorHandler for GpuRenderer {
29 fn on_render_error(&mut self, error: &cvkg_core::CvkgError) {
30 tracing::error!("[GpuRenderer] {error}");
31 self.render_error_count += 1;
32 }
33
34 fn on_fatal_error(&mut self, error: &cvkg_core::CvkgError) {
35 tracing::error!("[GpuRenderer FATAL] {error}");
36 self.has_fatal_error = true;
37 }
38
39 fn has_error(&self) -> bool {
40 self.has_fatal_error
41 }
42}
43
44impl cvkg_core::Renderer for GpuRenderer {
45 fn is_over_budget(&self) -> bool {
46 self.frame_budget.allow_degradation
47 && self.last_frame_start.elapsed().as_secs_f32() * 1000.0 > self.frame_budget.target_ms
48 }
49
50 fn text_scale_factor(&self) -> f32 {
51 self.current_scale_factor()
52 }
53
54 fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
55 tracing::info!(
56 "[Surtr] Pre-warming Mega-Heim with {} assets...",
57 assets.len()
58 );
59 for (name, data) in assets {
60 self.load_image_to_heim(&name, &data);
61 }
62 }
63
64 fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
65 self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
66 }
67
68 fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
69 self.fill_rect_with_full_params(
70 rect,
71 self.apply_opacity(color),
72 3,
73 None,
74 radius,
75 Rect {
76 x: 0.0,
77 y: 0.0,
78 width: 1.0,
79 height: 1.0,
80 },
81 );
82 }
83
84 fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
91 self.fill_glass_rect_with_intensity(rect, radius, blur_radius, 1.0);
92 }
93
94 fn fill_glass_rect_with_intensity(
98 &mut self,
99 rect: Rect,
100 radius: f32,
101 blur_radius: f32,
102 glass_intensity: f32,
103 ) {
104 self.fill_glass_rect_with_tint(
106 rect,
107 radius,
108 blur_radius,
109 [1.0, 1.0, 1.0, 0.4],
110 glass_intensity,
111 );
112 }
113
114 fn fill_glass_rect_with_tint(
117 &mut self,
118 rect: Rect,
119 radius: f32,
120 blur_radius: f32,
121 tint_color: [f32; 4],
122 glass_intensity: f32,
123 ) {
124 let gi = glass_intensity.clamp(0.0, 1.0);
125 let blur_strength = (blur_radius / 25.0).clamp(0.0, 4.0) * gi;
128
129 if self.current_z != 0.0 {
131 self.portal_regions.push_back(rect);
132 }
133
134 let prev_material = self.current_draw_material;
136 self.current_draw_material = cvkg_core::DrawMaterial::Glass {
137 blur_radius: blur_strength,
138 ior_override: 0.0,
139 glass_intensity: gi,
140 };
141
142 let fill_color = [
144 tint_color[0],
145 tint_color[1],
146 tint_color[2],
147 tint_color[3] * gi,
148 ];
149
150 self.fill_rect_with_full_params(
151 rect,
152 fill_color,
153 7, None,
155 radius,
156 Rect {
157 x: 0.0,
158 y: 0.0,
159 width: 1.0,
160 height: 1.0,
161 },
162 );
163
164 self.current_draw_material = prev_material;
165 }
166
167 fn fill_glass_rect_with_pressure(
168 &mut self,
169 rect: Rect,
170 radius: f32,
171 blur_radius: f32,
172 pressure: f32,
173 ) {
174 let p = pressure.clamp(0.0, 1.0);
176 self.fill_glass_rect_with_intensity(rect, radius, blur_radius * p, p);
177 }
178
179 fn set_default_background_color(&mut self, color: [f32; 4]) {
183 self.default_background_color = color;
184 }
185
186 fn fill_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4]) {
189 let prev_material = self.current_draw_material;
190 self.current_draw_material = cvkg_core::DrawMaterial::Opaque;
191 self.fill_rect_with_full_params(
192 rect,
193 self.apply_opacity(color),
194 0,
195 None,
196 rect.width.min(rect.height) * 0.22 * (n / 4.0),
197 Rect {
198 x: 0.0,
199 y: 0.0,
200 width: 1.0,
201 height: 1.0,
202 },
203 );
204 self.current_draw_material = prev_material;
205 }
206
207 fn stroke_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4], stroke_width: f32) {
209 let prev_material = self.current_draw_material;
210 self.current_draw_material = cvkg_core::DrawMaterial::Opaque;
211 self.fill_rect_with_full_params(
212 rect,
213 self.apply_opacity(color),
214 material_id::SQUIRCLE_STROKE,
215 None,
216 rect.width.min(rect.height) * 0.22 * (n / 4.0),
217 Rect {
218 x: stroke_width,
219 y: 0.0,
220 width: 0.0,
221 height: 0.0,
222 },
223 );
224 self.current_draw_material = prev_material;
225 }
226
227 fn draw_focus_ring(
230 &mut self,
231 rect: Rect,
232 radius: f32,
233 offset: f32,
234 width: f32,
235 color: [f32; 4],
236 ) {
237 let ring_rect = Rect {
238 x: rect.x - offset,
239 y: rect.y - offset,
240 width: rect.width + 2.0 * offset,
241 height: rect.height + 2.0 * offset,
242 };
243 self.stroke_squircle(ring_rect, 4.0, color, width);
244 }
245
246 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
247 self.fill_rect_with_full_params(
248 rect,
249 self.apply_opacity(color),
250 4,
251 None,
252 0.0,
253 Rect {
254 x: 0.0,
255 y: 0.0,
256 width: 1.0,
257 height: 1.0,
258 },
259 );
260 }
261
262 fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
263 self.fill_rect_with_full_params_and_slice(
264 rect,
265 self.apply_opacity(color),
266 material_id::MESH_3D,
267 None,
268 0.0,
269 Rect {
270 x: 0.0,
271 y: 0.0,
272 width: 1.0,
273 height: 1.0,
274 },
275 [rotation[0], rotation[1], rotation[2], 0.0],
276 [0.0, 0.0],
277 );
278 }
279
280 fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
281 let logical_w = self.current_width() as f32 / self.current_scale_factor();
283 let logical_h = self.current_height() as f32 / self.current_scale_factor();
284 let screen_uv = Rect {
285 x: rect.x / logical_w,
286 y: rect.y / logical_h,
287 width: rect.width / logical_w,
288 height: rect.height / logical_h,
289 };
290 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
293 }
294
295 fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
296 let margin = radius;
298 let glow_rect = Rect {
299 x: rect.x - margin,
300 y: rect.y - margin,
301 width: rect.width + 2.0 * margin,
302 height: rect.height + 2.0 * margin,
303 };
304 let glow_color = [color[0], color[1], color[2], intensity * 0.3];
305 self.fill_rect_with_full_params(
306 glow_rect,
307 self.apply_opacity(glow_color),
308 material_id::DROP_SHADOW,
309 None,
310 8.0,
311 Rect {
312 x: margin,
313 y: radius,
314 width: 0.0,
315 height: 0.0,
316 },
317 );
318 }
319
320 fn gungnir_soft(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
323 self.gungnir(rect, color, radius, intensity * 0.5);
324 }
325
326 fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
333 let margin = radius;
334 let glow_rect = Rect {
335 x: rect.x - margin,
336 y: rect.y - margin,
337 width: rect.width + 2.0 * margin,
338 height: rect.height + 2.0 * margin,
339 };
340 let uv_rect = Rect {
341 x: margin,
342 y: radius,
343 width: 0.0,
344 height: 0.0,
345 };
346 self.fill_rect_with_full_params(
347 glow_rect,
348 self.apply_opacity(color),
349 material_id::DROP_SHADOW,
350 None,
351 8.0,
352 uv_rect,
353 );
354 }
355
356 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
357 let c = self.apply_opacity(color);
358 self.fill_rect_with_full_params(
360 rect,
361 c,
362 material_id::SQUIRCLE_STROKE,
363 None,
364 0.0, Rect {
366 x: stroke_width,
367 y: 0.0,
368 width: 0.0,
369 height: 0.0,
370 },
371 );
372 }
373
374 fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
375 self.fill_rect_with_full_params(
376 rect,
377 self.apply_opacity(color),
378 material_id::SQUIRCLE_STROKE,
379 None,
380 radius,
381 Rect {
382 x: stroke_width,
383 y: 0.0,
384 width: 0.0,
385 height: 0.0,
386 },
387 );
388 }
389
390 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
391 let cx = rect.x + rect.width / 2.0;
393 let cy = rect.y + rect.height / 2.0;
394 let rx = rect.width / 2.0;
395 let ry = rect.height / 2.0;
396
397 let mut builder = lyon::path::Path::builder();
399 if rx > 0.0 && ry > 0.0 {
400 let segments = 64;
402 for i in 0..segments {
403 let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
404 let x = cx + rx * angle.cos();
405 let y = cy + ry * angle.sin();
406 if i == 0 {
407 builder.begin(lyon::math::point(x, y));
408 } else {
409 builder.line_to(lyon::math::point(x, y));
410 }
411 }
412 builder.close();
413 }
414 let path = builder.build();
415 self.stroke_path(&path, color, stroke_width);
416 }
417
418 fn draw_linear_gradient(
419 &mut self,
420 rect: Rect,
421 start_color: [f32; 4],
422 end_color: [f32; 4],
423 angle: f32,
424 ) {
425 self.fill_rect_with_full_params_and_slice(
426 rect,
427 self.apply_opacity(start_color),
428 15,
429 None,
430 0.0,
431 Rect {
432 x: angle,
433 y: 0.0,
434 width: 1.0,
435 height: 1.0,
436 },
437 end_color,
438 [0.0, 0.0],
439 );
440 }
441
442 fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
443 self.fill_rect_with_full_params_and_slice(
444 rect,
445 self.apply_opacity(inner_color),
446 material_id::RADIAL_GRADIENT,
447 None,
448 0.0,
449 Rect {
450 x: 0.0,
451 y: 0.0,
452 width: 1.0,
453 height: 1.0,
454 },
455 outer_color,
456 [0.0, 0.0],
457 );
458 }
459
460 fn draw_linear_gradient_multi(&mut self, rect: Rect, stops: &[[f32; 4]], angle: f32) {
461 self.draw_gradient_multi(rect, stops, angle, false);
462 }
463
464 fn draw_radial_gradient_multi(&mut self, rect: Rect, stops: &[[f32; 4]]) {
465 self.draw_gradient_multi(rect, stops, 0.0, true);
466 }
467
468 fn draw_drop_shadow(
469 &mut self,
470 rect: Rect,
471 radius: f32,
472 color: [f32; 4],
473 blur: f32,
474 spread: f32,
475 ) {
476 let margin = blur + spread;
477 let inflated = Rect {
478 x: rect.x - margin,
479 y: rect.y - margin,
480 width: rect.width + margin * 2.0,
481 height: rect.height + margin * 2.0,
482 };
483 self.fill_rect_with_full_params_and_slice(
485 inflated,
486 self.apply_opacity(color),
487 material_id::DROP_SHADOW,
488 None,
489 radius,
490 Rect {
491 x: margin,
492 y: blur,
493 width: 0.0,
494 height: 0.0,
495 },
496 [0.0, 0.0, 0.0, 1.0],
497 [0.0, 0.0],
498 );
499 }
500
501 fn stroke_dashed_rounded_rect(
502 &mut self,
503 rect: Rect,
504 radius: f32,
505 color: [f32; 4],
506 width: f32,
507 dash: f32,
508 gap: f32,
509 ) {
510 self.fill_rect_with_full_params(
511 rect,
512 self.apply_opacity(color),
513 material_id::DASHED_STROKE,
514 None,
515 radius,
516 Rect {
517 x: width,
518 y: dash,
519 width: gap,
520 height: 0.0,
521 },
522 );
523 }
524
525 fn draw_9slice(
526 &mut self,
527 image_name: &str,
528 rect: Rect,
529 left: f32,
530 top: f32,
531 right: f32,
532 bottom: f32,
533 ) {
534 let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
535 let tid = self.get_texture_id(image_name);
536 self.fill_rect_with_full_params(
537 rect,
538 c,
539 20,
540 tid,
541 bottom,
542 Rect {
543 x: left,
544 y: top,
545 width: right,
546 height: 0.0,
547 },
548 );
549 }
550
551 fn draw_line(
552 &mut self,
553 x1: f32,
554 y1: f32,
555 x2: f32,
556 y2: f32,
557 color: [f32; 4],
558 stroke_width: f32,
559 ) {
560 let dx = x2 - x1;
561 let dy = y2 - y1;
562 let len_sq = dx * dx + dy * dy;
563 if len_sq < 0.000001 {
564 return;
565 }
566 let len = len_sq.sqrt();
567 let half_w = stroke_width * 0.5;
568 let nx = -dy / len * half_w;
570 let ny = dx / len * half_w;
571 let points = [
573 [x1 + nx, y1 + ny],
574 [x2 + nx, y2 + ny],
575 [x2 - nx, y2 - ny],
576 [x1 - nx, y1 - ny],
577 ];
578 self.push_oriented_quad(
579 points,
580 color,
581 1,
582 Rect {
583 x: 0.0,
584 y: 0.0,
585 width: 1.0,
586 height: 1.0,
587 },
588 );
589 }
590
591 fn draw_image(&mut self, image_name: &str, rect: Rect) {
592 if !self.image_uv_registry.contains(image_name) {
594 tracing::warn!("[Surtr] draw_image: '{}' not loaded, skipping", image_name);
595 return;
596 }
597 let tid = self
598 .get_texture_id(image_name)
599 .or_else(|| self.get_texture_id("__mega_heim"));
600 let uv_rect = self
601 .image_uv_registry
602 .get(image_name)
603 .copied()
604 .unwrap_or(Rect {
605 x: 0.0,
606 y: 0.0,
607 width: 1.0,
608 height: 1.0,
609 });
610 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, uv_rect);
611 }
612
613 fn shape_rich_text(
614 &mut self,
615 spans: &[cvkg_runic_text::TextSpan],
616 max_width: Option<f32>,
617 align: cvkg_runic_text::TextAlign,
618 overflow: cvkg_runic_text::TextOverflow,
619 ) -> Option<cvkg_runic_text::ShapedText> {
620 self.shape_rich_text_impl(spans, max_width, align, overflow)
621 }
622
623 fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
624 self.draw_shaped_text_impl(shaped, x, y);
625 }
626
627 fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
628 self.fill_rect_with_full_params_and_slice(
629 rect,
630 [1.0, 1.0, 1.0, 1.0],
631 2,
632 Some(texture_id),
633 0.0,
634 Rect {
635 x: 0.0,
636 y: 0.0,
637 width: 1.0,
638 height: 1.0,
639 },
640 [0.0, 0.0, 0.0, 1.0],
641 [0.0, 0.0],
642 );
643 }
644
645 fn load_image(&mut self, name: &str, data: &[u8]) {
648 if self.image_uv_registry.contains(name) {
649 return;
650 }
651 let img_result = image::load_from_memory(data);
652 let img = match img_result {
653 Ok(img) => img.to_rgba8(),
654 Err(e) => {
655 tracing::error!("Failed to load image {}: {}", name, e);
656 image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 255, 255, 255]))
657 }
658 };
659 let (width, height) = img.dimensions();
660
661 let size = wgpu::Extent3d {
662 width,
663 height,
664 depth_or_array_layers: 1,
665 };
666 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
667 label: Some(&format!("Texture Array Layer: {}", name)),
668 size,
669 mip_level_count: 1,
670 sample_count: 1,
671 dimension: wgpu::TextureDimension::D2,
672 format: wgpu::TextureFormat::Rgba8UnormSrgb,
673 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
674 view_formats: &[],
675 });
676
677 self.queue.write_texture(
678 wgpu::TexelCopyTextureInfo {
679 texture: &texture,
680 mip_level: 0,
681 origin: wgpu::Origin3d::ZERO,
682 aspect: wgpu::TextureAspect::All,
683 },
684 &img,
685 wgpu::TexelCopyBufferLayout {
686 offset: 0,
687 bytes_per_row: Some(4 * width),
688 rows_per_image: Some(height),
689 },
690 size,
691 );
692
693 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
694
695 let index = if self.texture_registry.len() < 31 {
698 (self.texture_registry.len() + 1) as u32
699 } else {
700 if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
703 self.image_uv_registry.pop(&old_name);
704 old_index
705 } else {
706 tracing::warn!("[GPU] texture registry full and no LRU entry to evict");
707 return;
708 }
709 };
710
711 if index == 0 || index as usize >= self.texture_views.len() {
713 tracing::error!(
714 "[GPU] load_image: invalid texture index {} (registry has {} entries)",
715 index,
716 self.texture_registry.len()
717 );
718 return;
719 }
720
721 self.texture_views[index as usize] = view;
722 self.image_uv_registry.put(
723 name.to_string(),
724 Rect {
725 x: 0.0,
726 y: 0.0,
727 width: 1.0,
728 height: 1.0,
729 },
730 );
731 self.texture_registry.put(name.to_string(), index);
732 self.rebuild_texture_array_bind_group();
733 }
734
735 fn push_clip_rect(&mut self, rect: Rect) {
736 self.clip_stack.push(rect);
737 }
738
739 fn pop_clip_rect(&mut self) {
740 self.clip_stack.pop();
741 }
742
743 fn current_clip_rect(&self) -> Rect {
744 self.clip_stack.last().copied().unwrap_or(Rect::new(
745 0.0,
746 0.0,
747 self.current_width() as f32,
748 self.current_height() as f32,
749 ))
750 }
751
752 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
753 use crate::types::{DrawCall, MemoEntry};
766
767 let should_skip = self
768 .memo_cache
769 .get(&id)
770 .is_some_and(|entry| entry.hash == data_hash);
771
772 if should_skip {
773 if let Some(entry) = self.memo_cache.get(&id) {
775 let i_offset = self.indices.len() as u32;
776 let inst_offset = self.instance_data.len() as u32;
777
778 self.vertices.extend_from_slice(&entry.vertices);
779 self.indices.extend_from_slice(&entry.indices);
780 self.instance_data.extend_from_slice(&entry.instance_data);
781
782 for dc in &entry.draw_calls {
783 let mut replayed = dc.clone();
784 replayed.index_start += i_offset;
788 replayed.instance_start += inst_offset;
789 self.draw_calls.push(replayed);
790 }
791 }
792 } else {
793 let v_start = self.vertices.len();
795 let i_start = self.indices.len();
796 let inst_start = self.instance_data.len();
797 let dc_start = self.draw_calls.len();
798
799 render_fn(self);
800
801 let draw_calls: Vec<DrawCall> = self.draw_calls[dc_start..]
803 .iter()
804 .map(|dc| {
805 let mut remapped = dc.clone();
806 remapped.index_start = remapped.index_start.saturating_sub(i_start as u32);
810 remapped.instance_start =
811 remapped.instance_start.saturating_sub(inst_start as u32);
812 remapped
813 })
814 .collect();
815
816 let entry = MemoEntry {
817 hash: data_hash,
818 frame_gen: self.frame_generation,
819 vertices: self.vertices[v_start..].to_vec(),
820 indices: self.indices[i_start..].to_vec(),
821 instance_data: self.instance_data[inst_start..].to_vec(),
822 draw_calls,
823 };
824
825 self.memo_cache.insert(id, entry);
826 }
827 }
828
829 fn snapshot_render_state(&self) -> RenderStateSnapshot {
830 RenderStateSnapshot {
831 clip_depth: self.clip_stack.len() as u32,
832 opacity_depth: self.opacity_stack.len() as u32,
833 slice_depth: self.slice_stack.len() as u32,
834 shadow_depth: self.shadow_stack.len() as u32,
835 transform_depth: self.transform_stack.len() as u32,
836 vnode_depth: self.vnode_stack.len() as u32,
837 }
838 }
839
840 fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
841 while self.clip_stack.len() as u32 > snap.clip_depth {
843 self.clip_stack.pop();
844 }
845 while self.opacity_stack.len() as u32 > snap.opacity_depth {
846 self.opacity_stack.pop();
847 }
848 while self.slice_stack.len() as u32 > snap.slice_depth {
849 self.slice_stack.pop();
850 }
851 while self.shadow_stack.len() as u32 > snap.shadow_depth {
852 self.shadow_stack.pop();
853 }
854 while self.transform_stack.len() as u32 > snap.transform_depth {
855 self.transform_stack.pop();
856 }
857 while self.vnode_stack.len() as u32 > snap.vnode_depth {
858 self.vnode_stack.pop();
859 }
860 }
861
862 fn push_opacity(&mut self, opacity: f32) {
863 let current = self.opacity_stack.last().copied().unwrap_or(1.0);
864 self.opacity_stack.push(current * opacity);
865 }
866
867 fn pop_opacity(&mut self) {
868 self.opacity_stack.pop();
869 }
870
871 fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
872 self.shadow_stack.push(ShadowState {
873 radius,
874 color,
875 _offset: offset,
876 });
877 }
878
879 fn pop_shadow(&mut self) {
880 self.shadow_stack.pop();
881 }
882
883 fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
884 let c = rotation.cos();
885 let sn = rotation.sin();
886 let affine = glam::Mat3::from_cols(
887 glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
888 glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
889 glam::Vec3::new(translation[0], translation[1], 1.0),
890 );
891
892 let parent = self
893 .transform_stack
894 .last()
895 .copied()
896 .unwrap_or(glam::Mat3::IDENTITY);
897 self.transform_stack.push(parent * affine);
898 }
899
900 fn push_affine(&mut self, transform: [f32; 6]) {
901 let affine = glam::Mat3::from_cols(
902 glam::Vec3::new(transform[0], transform[1], 0.0),
903 glam::Vec3::new(transform[2], transform[3], 0.0),
904 glam::Vec3::new(transform[4], transform[5], 1.0),
905 );
906 let parent = self
907 .transform_stack
908 .last()
909 .copied()
910 .unwrap_or(glam::Mat3::IDENTITY);
911 self.transform_stack.push(parent * affine);
912 }
913
914 fn pop_transform(&mut self) {
915 self.transform_stack.pop();
916 }
917
918 fn set_theme(&mut self, theme: ColorTheme) {
919 self.current_theme = theme;
920 self.queue
921 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
922 }
923
924 fn push_theme(&mut self, theme: ColorTheme) {
925 let prev = self.current_theme;
926 self.current_theme = theme;
927 self.theme_stack.push(prev);
928 self.queue
929 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
930 cvkg_core::set_theme_context(cvkg_core::ThemeContext::from_color_theme(&theme));
931 }
932
933 fn pop_theme(&mut self) {
934 if let Some(parent_theme) = self.theme_stack.pop() {
935 self.current_theme = parent_theme;
936 self.queue
937 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&parent_theme));
938 cvkg_core::set_theme_context(cvkg_core::ThemeContext::from_color_theme(&parent_theme));
939 }
940 }
941
942 fn current_theme(&self) -> ColorTheme {
943 self.current_theme
944 }
945
946 fn set_rage(&mut self, rage: f32) {
947 self.current_scene.berzerker_rage = rage;
948 }
950
951 fn set_fireball_pos(&mut self, pos: [f32; 2]) {
952 self.current_scene.fireball_pos = pos;
953 }
954
955 fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
956 self.current_scene.shatter_origin = origin;
957 self.current_scene.shatter_time = self.current_scene.time;
958 self.current_scene.shatter_force = force;
959 }
960
961 fn set_scene_preset(&mut self, preset: u32) {
962 self.current_scene.scene_type = preset;
963 }
964
965 fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
968 self.slice_stack.push((angle, offset));
969 }
970
971 fn pop_mjolnir_slice(&mut self) {
973 self.slice_stack.pop();
974 }
975
976 fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
977 self.shatter_internal(rect, pieces, force, color, 8);
978 }
979
980 fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
981 self.shatter_internal(rect, pieces, force, color, 11);
982 }
983
984 fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
985 self.recursive_bolt(from, to, 4, color);
986 }
987
988 fn dispatch_particles(
989 &mut self,
990 origin: [f32; 2],
991 count: u32,
992 effect_type: &str,
993 color: [f32; 4],
994 ) {
995 use crate::types::{GpuParticle, MAX_PARTICLES};
996
997 let dt = self.current_scene.delta_time;
998 let now = std::time::Instant::now();
999
1000 let (speed_range, life_range, spread_angle) = match effect_type {
1002 "firework" => (100.0..300.0, 1.0..2.5, std::f32::consts::TAU),
1003 "spark" => (50.0..150.0, 0.5..1.5, std::f32::consts::PI),
1004 "rain" => (20.0..80.0, 1.0..3.0, std::f32::consts::FRAC_PI_4),
1005 "data_stream" => (80.0..200.0, 0.8..2.0, std::f32::consts::FRAC_PI_6),
1006 "bubble" => (10.0..40.0, 2.0..4.0, std::f32::consts::TAU),
1007 _ => (30.0..120.0, 1.0..2.0, std::f32::consts::TAU),
1008 };
1009
1010 let count = count.min((MAX_PARTICLES - self.particles.count as usize) as u32);
1011 if count == 0 {
1012 return;
1013 }
1014
1015 let mut rng_state = (now.elapsed().as_nanos() as u64)
1016 .wrapping_mul(6364136223846793005)
1017 .wrapping_add(1442695040888963407);
1018 let mut rand_f32 = |range: std::ops::Range<f32>| -> f32 {
1019 rng_state = rng_state
1020 .wrapping_mul(6364136223846793005)
1021 .wrapping_add(1442695040888963407);
1022 let t = (rng_state >> 33) as f32 / (1u64 << 31) as f32;
1023 range.start + t * (range.end - range.start)
1024 };
1025
1026 for _ in 0..count {
1027 let angle = rand_f32(0.0..spread_angle);
1028 let speed = rand_f32(speed_range.clone());
1029 let life = rand_f32(life_range.clone());
1030 let vx = angle.cos() * speed;
1031 let vy = angle.sin() * speed;
1032
1033 let particle = GpuParticle {
1034 pos_vel: [origin[0], origin[1], vx, vy],
1035 color_life: [color[0], color[1], color[2], life],
1036 };
1037 self.particles.staging.push(particle);
1038 }
1039
1040 tracing::debug!(
1041 "[Surtr] dispatch_particles: {} {} particles at {:?} (staged, {} total pending)",
1042 count,
1043 effect_type,
1044 origin,
1045 self.particles.staging.len()
1046 );
1047 }
1048
1049 fn draw_hologram(&mut self, rect: Rect, hologram_id: &str, time: f32) {
1050 use std::hash::{Hash, Hasher};
1051 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1052 hologram_id.hash(&mut hasher);
1053 let id_hash = hasher.finish() as u32;
1054
1055 tracing::debug!(
1056 "[Surtr] draw_hologram: {} at {:?} t={} (hologram pipeline)",
1057 hologram_id,
1058 rect,
1059 time
1060 );
1061
1062 self.hologram_instances
1063 .push(crate::renderer::HologramInstance {
1064 rect,
1065 id_hash,
1066 time,
1067 });
1068 self.volumetric_enabled = true;
1069 }
1070
1071 fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
1072 let size = wgpu::Extent3d {
1073 width,
1074 height,
1075 depth_or_array_layers: 1,
1076 };
1077 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1078 label: Some(id),
1079 size,
1080 mip_level_count: 1,
1081 sample_count: 1,
1082 dimension: wgpu::TextureDimension::D2,
1083 format: wgpu::TextureFormat::R32Float,
1084 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1085 view_formats: &[],
1086 });
1087 self.queue.write_texture(
1088 wgpu::TexelCopyTextureInfo {
1089 texture: &texture,
1090 mip_level: 0,
1091 origin: wgpu::Origin3d::ZERO,
1092 aspect: wgpu::TextureAspect::All,
1093 },
1094 bytemuck::cast_slice(data),
1095 wgpu::TexelCopyBufferLayout {
1096 offset: 0,
1097 bytes_per_row: Some(4 * width),
1098 rows_per_image: Some(height),
1099 },
1100 size,
1101 );
1102 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1103 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1106 layout: &self.texture_bind_group_layout,
1107 entries: &[
1108 wgpu::BindGroupEntry {
1109 binding: 0,
1110 resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 32]),
1112 },
1113 wgpu::BindGroupEntry {
1114 binding: 1,
1115 resource: wgpu::BindingResource::Sampler(&self.linear_sampler),
1116 },
1117 ],
1118 label: Some(id),
1119 });
1120 self.texture_bind_groups.push(bind_group);
1121 let tid = (self.texture_bind_groups.len() - 1) as u32;
1122 self.texture_registry.put(id.to_string(), tid);
1123 }
1124
1125 fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
1126 let tid = self.get_texture_id(texture_id);
1127 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
1128 }
1129
1130 fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
1131 let base_idx = self.vertices.len() as u32;
1132
1133 for i in 0..mesh.vertices.len() {
1134 let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1135 let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1136
1137 self.vertices.push(Vertex {
1138 position: pos.to_array(),
1139 normal: norm.to_array(),
1140 uv: [0.0, 0.0],
1141 color,
1142 material_id: 13, radius: 0.0,
1144 slice: [0.0, 0.0, 0.0, 1.0],
1145 logical: [0.0, 0.0],
1146 size: [0.0, 0.0],
1147 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1148 tex_index: 0,
1149 });
1150 }
1151
1152 for idx in &mesh.indices {
1153 self.indices.push(base_idx + idx);
1154 }
1155
1156 let (translation, scale_transform, rotation, _, _) = self.current_transform();
1157
1158 if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
1159 self.current_texture_id = None;
1160
1161 self.instance_data.push(InstanceData {
1162 translation,
1163 scale: scale_transform,
1164 rotation,
1165 blur_radius: 0.0,
1166 ior_override: 0.0,
1167 glass_intensity: 1.0,
1168 });
1169 self.draw_calls.push(DrawCall {
1170 target_id: None,
1171 panel_id: self.current_panel_id,
1172 texture_id: None,
1173 scissor_rect: self.clip_stack.last().copied(),
1174 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1175 index_count: mesh.indices.len() as u32,
1176 instance_count: 1,
1177 material: cvkg_core::DrawMaterial::Opaque,
1178 instance_start: (self.instance_data.len() - 1) as u32,
1179 draw_order: 0,
1180 });
1181 } else {
1182 self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1183 }
1184 }
1185
1186 fn draw_mesh_3d(
1187 &mut self,
1188 mesh: &Mesh,
1189 material: &cvkg_core::Material3D,
1190 transform: &cvkg_core::Transform3D,
1191 ) {
1192 let base_idx = self.vertices.len() as u32;
1193 let model_matrix = transform.to_matrix();
1194
1195 for i in 0..mesh.vertices.len() {
1196 let pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1197 let norm = model_matrix.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1198 let raw_uv = mesh.tex_coords.get(i).copied().unwrap_or([0.0, 0.0]);
1200 let uv = [
1201 raw_uv[0] * material.uv_scale[0] + material.uv_offset[0],
1202 raw_uv[1] * material.uv_scale[1] + material.uv_offset[1],
1203 ];
1204
1205 self.vertices.push(Vertex {
1206 position: [pos.x, pos.y, pos.z],
1207 normal: [norm.x, norm.y, norm.z],
1208 uv,
1209 color: material.base_color,
1210 material_id: 13, radius: 0.0,
1212 slice: [material.metallic, material.roughness, material.opacity, 1.0],
1213 logical: [0.0, 0.0],
1214 size: [0.0, 0.0],
1215 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1216 tex_index: 0,
1217 });
1218 }
1219
1220 for idx in &mesh.indices {
1221 self.indices.push(base_idx + idx);
1222 }
1223
1224 self.instance_data.push(InstanceData {
1225 translation: [0.0, 0.0],
1226 scale: [1.0, 1.0],
1227 rotation: 0.0,
1228 blur_radius: 0.0,
1229 ior_override: 0.0,
1230 glass_intensity: 1.0,
1231 });
1232
1233 let row0 = model_matrix.row(0);
1238 let row1 = model_matrix.row(1);
1239 let row2 = model_matrix.row(2);
1240 self.instance_data_3d.push(InstanceData3D {
1241 model_row0: [row0.x, row0.y, row0.z, row0.w],
1242 model_row1: [row1.x, row1.y, row1.z, row1.w],
1243 model_row2: [row2.x, row2.y, row2.z, row2.w],
1244 material_overrides: [material.metallic, material.roughness, 0.0, material.opacity],
1245 uv_scale: material.uv_scale,
1246 uv_offset: material.uv_offset,
1247 });
1248
1249 self.draw_calls.push(DrawCall {
1250 target_id: None,
1251 panel_id: self.current_panel_id,
1252 texture_id: None,
1253 scissor_rect: self.clip_stack.last().copied(),
1254 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1255 index_count: mesh.indices.len() as u32,
1256 instance_count: 1,
1257 material: cvkg_core::DrawMaterial::Opaque,
1258 instance_start: (self.instance_data.len() - 1) as u32,
1259 draw_order: 0,
1260 });
1261 }
1262
1263 fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1264 self.current_scene.proj = camera.projection_matrix();
1265 self.current_scene.view = camera.view_matrix();
1266 self.current_scene.camera_pos = camera.position.into();
1267 }
1268
1269 fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1270 let model_matrix = transform.to_matrix();
1271 let parent = self
1272 .transform_stack_3d
1273 .last()
1274 .copied()
1275 .unwrap_or(glam::Mat4::IDENTITY);
1276 self.transform_stack_3d.push(parent * model_matrix);
1277 }
1278
1279 fn pop_transform_3d(&mut self) {
1280 self.transform_stack_3d.pop();
1281 }
1282
1283 fn render_scene_node_3d(
1289 &mut self,
1290 position: [f32; 3],
1291 rotation: [f32; 4],
1292 scale: [f32; 3],
1293 color: [f32; 4],
1294 meshes: &[Mesh],
1295 ) {
1296 let transform = cvkg_core::Transform3D {
1297 position: glam::Vec3::from(position),
1298 rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1299 scale: glam::Vec3::from(scale),
1300 };
1301 if meshes.is_empty() {
1303 let h = 0.5f32;
1305 let cube = Mesh {
1306 vertices: vec![
1307 [-h, -h, -h],
1308 [h, -h, -h],
1309 [h, h, -h],
1310 [-h, h, -h],
1311 [-h, -h, h],
1312 [h, -h, h],
1313 [h, h, h],
1314 [-h, h, h],
1315 ],
1316 normals: vec![
1317 [0.0, 0.0, -1.0],
1318 [0.0, 0.0, -1.0],
1319 [0.0, 0.0, -1.0],
1320 [0.0, 0.0, -1.0],
1321 [0.0, 0.0, 1.0],
1322 [0.0, 0.0, 1.0],
1323 [0.0, 0.0, 1.0],
1324 [0.0, 0.0, 1.0],
1325 [0.0, -1.0, 0.0],
1326 [0.0, -1.0, 0.0],
1327 [0.0, -1.0, 0.0],
1328 [0.0, -1.0, 0.0],
1329 [1.0, 0.0, 0.0],
1330 [1.0, 0.0, 0.0],
1331 [1.0, 0.0, 0.0],
1332 [1.0, 0.0, 0.0],
1333 [0.0, 1.0, 0.0],
1334 [0.0, 1.0, 0.0],
1335 [0.0, 1.0, 0.0],
1336 [0.0, 1.0, 0.0],
1337 [-1.0, 0.0, 0.0],
1338 [-1.0, 0.0, 0.0],
1339 [-1.0, 0.0, 0.0],
1340 [-1.0, 0.0, 0.0],
1341 ],
1342 indices: vec![
1343 0, 1, 2, 0, 2, 3, 5, 4, 7, 5, 7, 6, 4, 0, 3, 4, 3, 7, 1, 5, 6, 1, 6, 2, 3, 2, 6, 3, 6, 7, 4, 5, 1, 4, 1, 0, ],
1350 tex_coords: vec![[0.0, 0.0]; 8],
1351 };
1352 let material = cvkg_core::Material3D {
1353 base_color: color,
1354 base_color_texture: None,
1355 normal_map_texture: None,
1356 metallic_roughness_texture: None,
1357 metallic: 0.0,
1358 roughness: 0.5,
1359 emissive: [0.0, 0.0, 0.0],
1360 opacity: color[3],
1361 uv_scale: [1.0, 1.0],
1362 uv_offset: [0.0, 0.0],
1363 };
1364 self.draw_mesh_3d(&cube, &material, &transform);
1365 } else {
1366 let material = cvkg_core::Material3D {
1367 base_color: color,
1368 base_color_texture: None,
1369 normal_map_texture: None,
1370 metallic_roughness_texture: None,
1371 metallic: 0.0,
1372 roughness: 0.5,
1373 emissive: [0.0, 0.0, 0.0],
1374 opacity: color[3],
1375 uv_scale: [1.0, 1.0],
1376 uv_offset: [0.0, 0.0],
1377 };
1378 self.draw_mesh_3d(&meshes[0], &material, &transform);
1379 }
1380 }
1381
1382 fn register_shared_element(&mut self, id: &str, rect: Rect) {
1383 self.shared_elements.put(id.to_string(), rect);
1384 }
1385
1386 fn set_z_index(&mut self, z: f32) {
1387 self.current_z = z;
1388 }
1389
1390 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1391 self.current_draw_material = material;
1392 }
1393
1394 fn current_material(&self) -> cvkg_core::DrawMaterial {
1395 self.current_draw_material
1396 }
1397
1398 fn get_z_index(&self) -> f32 {
1399 self.current_z
1400 }
1401
1402 fn request_redraw(&mut self) {
1403 self.redraw_requested = true;
1404 }
1405
1406 fn enter_portal(&mut self, z_index: i32) {
1418 self.portal_theme_stack.push(self.current_theme);
1422 self.current_z = z_index as f32;
1423 }
1424
1425 fn exit_portal(&mut self) {
1429 self.current_z = 0.0;
1430 if let Some(restored_theme) = self.portal_theme_stack.pop() {
1432 self.current_theme = restored_theme;
1433 self.queue
1434 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&restored_theme));
1435 cvkg_core::set_theme_context(cvkg_core::ThemeContext::from_color_theme(&restored_theme));
1436 }
1437 }
1438
1439 fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1440 self.vnode_stack.push((rect, name));
1441 }
1442
1443 fn pop_vnode(&mut self) {
1444 self.vnode_stack.pop();
1445 }
1446
1447 fn register_handler(
1448 &mut self,
1449 event_type: &str,
1450 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1451 ) {
1452 self.event_handlers
1453 .entry(event_type.to_string())
1454 .or_insert_with(Vec::new)
1455 .push(handler);
1456 }
1457
1458 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1459 GpuRenderer::load_svg(self, name, svg_data);
1460 }
1461
1462 fn draw_svg(&mut self, name: &str, rect: Rect) {
1463 GpuRenderer::draw_svg(self, name, rect, None, 0);
1464 }
1465 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, animation_time_offset: f32) {
1466 GpuRenderer::draw_svg_with_offset(self, name, rect, None, 0, animation_time_offset);
1467 }
1468
1469 fn draw_svg_with_order(&mut self, name: &str, rect: Rect, draw_order: i32) {
1472 GpuRenderer::draw_svg_with_order(self, name, rect, None, 0, 0.0, draw_order);
1473 }
1474
1475 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1476 let tree = self
1477 .svg
1478 .tree_cache
1479 .get(name)
1480 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1481 let config = cvkg_svg_serialize::SerializerConfig::default();
1482 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1483 serializer
1484 .serialize(tree)
1485 .map_err(|e| format!("SVG serialization failed: {}", e))
1486 }
1487
1488 fn apply_svg_filter(
1489 &mut self,
1490 name: &str,
1491 filter_id: &str,
1492 _region: Rect,
1493 ) -> Result<String, String> {
1494 let tree = self
1495 .svg
1496 .tree_cache
1497 .get(name)
1498 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1499 let _filter = Self::find_filter(tree, filter_id)
1500 .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1501 let config = cvkg_svg_serialize::SerializerConfig::default();
1502 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1503 serializer
1504 .serialize(tree)
1505 .map_err(|e| format!("SVG filter serialization failed: {}", e))
1506 }
1507
1508 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1509 self.measure_text_impl(text, size)
1510 }
1511
1512 fn draw_text(&mut self, text: &str, rect: &Rect, size: f32, color: [f32; 4], h_align: cvkg_core::TextHAlign, v_align: cvkg_core::TextVAlign) {
1513 self.draw_text_impl(text, rect, size, color, h_align, v_align);
1514 }
1515}
1516
1517impl GpuRenderer {
1520 pub fn clear_event_handlers(&mut self) {
1523 self.event_handlers.clear();
1524 }
1525
1526 pub fn clear_text_cache(&mut self) {
1528 self.clear_text_cache_impl();
1529 }
1530
1531 pub fn get_handlers(
1533 &self,
1534 event_type: &str,
1535 ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1536 self.event_handlers.get(event_type)
1537 }
1538
1539 pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1543 let m = self
1546 .transform_stack
1547 .last()
1548 .copied()
1549 .unwrap_or(glam::Mat3::IDENTITY);
1550 let t = [m.z_axis.x, m.z_axis.y];
1551 let a = m.x_axis.x;
1553 let b = m.x_axis.y;
1554 let c = m.y_axis.x;
1555 let d = m.y_axis.y;
1556 let sx = (a * a + b * b).sqrt();
1557 let sy = (c * c + d * d).sqrt();
1558 let rotation = b.atan2(a);
1559 let skew_x = (a * c + b * d) / (sx * sy); (t, [sx, sy], rotation, skew_x, 0.0)
1562 }
1563
1564 pub(crate) fn current_transform_3d(&self) -> glam::Mat4 {
1566 self.transform_stack_3d
1567 .last()
1568 .copied()
1569 .unwrap_or(glam::Mat4::IDENTITY)
1570 }
1571
1572 pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1573 self.stroke_path_impl(path, color, stroke_width);
1574 }
1575}
1576
1577#[cfg(test)]
1578mod transform_3d_tests {
1579 use glam::{Mat4, Vec3, Quat};
1580 use cvkg_core::Transform3D;
1581
1582 #[test]
1583 fn test_transform3d_to_matrix_translation() {
1584 let transform = Transform3D {
1585 position: Vec3::new(10.0, 20.0, 30.0),
1586 ..Default::default()
1587 };
1588 let m = transform.to_matrix();
1589 assert!((m.w_axis.x - 10.0).abs() < 0.001);
1590 assert!((m.w_axis.y - 20.0).abs() < 0.001);
1591 assert!((m.w_axis.z - 30.0).abs() < 0.001);
1592 }
1593
1594 #[test]
1595 fn test_transform3d_to_matrix_scale() {
1596 let transform = Transform3D {
1597 scale: Vec3::new(2.0, 3.0, 4.0),
1598 ..Default::default()
1599 };
1600 let m = transform.to_matrix();
1601 assert!((m.x_axis.x - 2.0).abs() < 0.001);
1602 assert!((m.y_axis.y - 3.0).abs() < 0.001);
1603 assert!((m.z_axis.z - 4.0).abs() < 0.001);
1604 }
1605
1606 #[test]
1607 fn test_transform3d_hierarchical_multiplication() {
1608 let parent = Transform3D {
1609 position: Vec3::new(10.0, 0.0, 0.0),
1610 ..Default::default()
1611 };
1612 let child = Transform3D {
1613 position: Vec3::new(5.0, 0.0, 0.0),
1614 ..Default::default()
1615 };
1616 let parent_m = parent.to_matrix();
1617 let child_m = child.to_matrix();
1618 let combined = parent_m * child_m;
1619 assert!((combined.w_axis.x - 15.0).abs() < 0.001);
1620 }
1621
1622 #[test]
1623 fn test_transform3d_rotation() {
1624 let transform = Transform3D {
1625 rotation: Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1626 ..Default::default()
1627 };
1628 let m = transform.to_matrix();
1629 assert!((m.x_axis.x).abs() < 0.001);
1631 assert!((m.x_axis.y - 1.0).abs() < 0.001);
1632 assert!((m.y_axis.x + 1.0).abs() < 0.001);
1633 assert!((m.y_axis.y).abs() < 0.001);
1634 }
1635}