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 let view_depth =
1197 (glam::Vec3::from(self.current_scene.camera_pos) - transform.position).length();
1198
1199 for i in 0..mesh.vertices.len() {
1200 let pos = glam::Vec3::from(mesh.vertices[i]);
1201 let norm = glam::Vec3::from(mesh.normals[i]);
1202 let raw_uv = mesh.tex_coords.get(i).copied().unwrap_or([0.0, 0.0]);
1204 let uv = [
1205 raw_uv[0] * material.uv_scale[0] + material.uv_offset[0],
1206 raw_uv[1] * material.uv_scale[1] + material.uv_offset[1],
1207 ];
1208
1209 self.vertices.push(Vertex {
1210 position: [pos.x, pos.y, pos.z],
1211 normal: [norm.x, norm.y, norm.z],
1212 uv,
1213 color: material.base_color,
1214 material_id: 13, radius: 0.0,
1216 slice: [material.metallic, material.roughness, material.opacity, 1.0],
1217 logical: [0.0, 0.0],
1218 size: [0.0, 0.0],
1219 clip: mesh
1220 .tangents
1221 .get(i)
1222 .copied()
1223 .unwrap_or([0.0, 0.0, 1.0, 1.0]),
1224 tex_index: 0,
1225 });
1226 }
1227
1228 for idx in &mesh.indices {
1229 self.indices.push(base_idx + idx);
1230 }
1231
1232 self.instance_data.push(InstanceData {
1233 translation: [0.0, 0.0],
1234 scale: [1.0, 1.0],
1235 rotation: 0.0,
1236 blur_radius: 0.0,
1237 ior_override: 0.0,
1238 glass_intensity: 1.0,
1239 });
1240
1241 let row0 = model_matrix.row(0);
1243 let row1 = model_matrix.row(1);
1244 let row2 = model_matrix.row(2);
1245 let instance_index = self.instance_data_3d.len() as u32;
1246 self.instance_data_3d.push(InstanceData3D {
1247 model_row0: [row0.x, row0.y, row0.z, row0.w],
1248 model_row1: [row1.x, row1.y, row1.z, row1.w],
1249 model_row2: [row2.x, row2.y, row2.z, row2.w],
1250 material_overrides: [material.metallic, material.roughness, 0.0, material.opacity],
1251 uv_scale: material.uv_scale,
1252 uv_offset: material.uv_offset,
1253 });
1254
1255 let gpu_mesh = crate::passes::shadow::GpuMesh3d {
1257 vertex_buffer: self.geometry_buffers.vertex_buffer.clone(),
1258 index_buffer: self.geometry_buffers.index_buffer.clone(),
1259 index_count: mesh.indices.len() as u32,
1260 transform: model_matrix,
1261 view_depth,
1262 instance_index,
1263 };
1264
1265 if material.opacity < 1.0 {
1266 self.pending_transparent_instances_3d.push(gpu_mesh);
1267 } else {
1268 self.pending_mesh_instances_3d.push(gpu_mesh);
1269 }
1270
1271 self.draw_calls.push(DrawCall {
1272 target_id: None,
1273 panel_id: self.current_panel_id,
1274 texture_id: None,
1275 scissor_rect: self.clip_stack.last().copied(),
1276 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1277 index_count: mesh.indices.len() as u32,
1278 instance_count: 1,
1279 material: cvkg_core::DrawMaterial::Opaque,
1280 instance_start: (self.instance_data.len() - 1) as u32,
1281 draw_order: 0,
1282 });
1283 }
1284
1285 fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1286 self.current_scene.proj = camera.projection_matrix();
1287 self.current_scene.view = camera.view_matrix();
1288 self.current_scene.camera_pos = camera.position.into();
1289 }
1290
1291 fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1292 let model_matrix = transform.to_matrix();
1293 let parent = self
1294 .transform_stack_3d
1295 .last()
1296 .copied()
1297 .unwrap_or(glam::Mat4::IDENTITY);
1298 self.transform_stack_3d.push(parent * model_matrix);
1299 }
1300
1301 fn pop_transform_3d(&mut self) {
1302 self.transform_stack_3d.pop();
1303 }
1304
1305 fn render_scene_node_3d(
1314 &mut self,
1315 position: [f32; 3],
1316 rotation: [f32; 4],
1317 scale: [f32; 3],
1318 color: [f32; 4],
1319 meshes: &[Mesh],
1320 ) {
1321 let transform = cvkg_core::Transform3D {
1322 position: glam::Vec3::from(position),
1323 rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1324 scale: glam::Vec3::from(scale),
1325 };
1326 if meshes.is_empty() {
1328 let h = 0.5f32;
1330 let cube = Mesh {
1331 vertices: vec![
1332 [-h, -h, -h],
1333 [h, -h, -h],
1334 [h, h, -h],
1335 [-h, h, -h],
1336 [-h, -h, h],
1337 [h, -h, h],
1338 [h, h, h],
1339 [-h, h, h],
1340 ],
1341 normals: vec![
1342 [0.0, 0.0, -1.0],
1343 [0.0, 0.0, -1.0],
1344 [0.0, 0.0, -1.0],
1345 [0.0, 0.0, -1.0],
1346 [0.0, 0.0, 1.0],
1347 [0.0, 0.0, 1.0],
1348 [0.0, 0.0, 1.0],
1349 [0.0, 0.0, 1.0],
1350 [0.0, -1.0, 0.0],
1351 [0.0, -1.0, 0.0],
1352 [0.0, -1.0, 0.0],
1353 [0.0, -1.0, 0.0],
1354 [1.0, 0.0, 0.0],
1355 [1.0, 0.0, 0.0],
1356 [1.0, 0.0, 0.0],
1357 [1.0, 0.0, 0.0],
1358 [0.0, 1.0, 0.0],
1359 [0.0, 1.0, 0.0],
1360 [0.0, 1.0, 0.0],
1361 [0.0, 1.0, 0.0],
1362 [-1.0, 0.0, 0.0],
1363 [-1.0, 0.0, 0.0],
1364 [-1.0, 0.0, 0.0],
1365 [-1.0, 0.0, 0.0],
1366 ],
1367 indices: vec![
1368 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, ],
1375 tex_coords: vec![[0.0, 0.0]; 8],
1376 tangents: Vec::new(),
1377 };
1378 let mut cube = cube;
1379 cube.tangents = cube.compute_tangents();
1380 let material = cvkg_core::Material3D {
1381 base_color: color,
1382 base_color_texture: None,
1383 normal_map_texture: None,
1384 metallic_roughness_texture: None,
1385 metallic: 0.0,
1386 roughness: 0.5,
1387 emissive: [0.0, 0.0, 0.0],
1388 opacity: color[3],
1389 uv_scale: [1.0, 1.0],
1390 uv_offset: [0.0, 0.0],
1391 };
1392 self.submit_mesh_3d(&cube, &material, &transform);
1393 } else {
1394 let material = cvkg_core::Material3D {
1395 base_color: color,
1396 base_color_texture: None,
1397 normal_map_texture: None,
1398 metallic_roughness_texture: None,
1399 metallic: 0.0,
1400 roughness: 0.5,
1401 emissive: [0.0, 0.0, 0.0],
1402 opacity: color[3],
1403 uv_scale: [1.0, 1.0],
1404 uv_offset: [0.0, 0.0],
1405 };
1406 self.submit_mesh_3d(&meshes[0], &material, &transform);
1407 }
1408 }
1409
1410 fn register_shared_element(&mut self, id: &str, rect: Rect) {
1411 self.shared_elements.put(id.to_string(), rect);
1412 }
1413
1414 fn set_z_index(&mut self, z: f32) {
1415 self.current_z = z;
1416 }
1417
1418 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1419 self.current_draw_material = material;
1420 }
1421
1422 fn current_material(&self) -> cvkg_core::DrawMaterial {
1423 self.current_draw_material
1424 }
1425
1426 fn get_z_index(&self) -> f32 {
1427 self.current_z
1428 }
1429
1430 fn request_redraw(&mut self) {
1431 self.redraw_requested = true;
1432 }
1433
1434 fn enter_portal(&mut self, z_index: i32) {
1446 self.portal_theme_stack.push(self.current_theme);
1450 self.current_z = z_index as f32;
1451 }
1452
1453 fn exit_portal(&mut self) {
1457 self.current_z = 0.0;
1458 if let Some(restored_theme) = self.portal_theme_stack.pop() {
1460 self.current_theme = restored_theme;
1461 self.queue
1462 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&restored_theme));
1463 cvkg_core::set_theme_context(cvkg_core::ThemeContext::from_color_theme(
1464 &restored_theme,
1465 ));
1466 }
1467 }
1468
1469 fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1470 self.vnode_stack.push((rect, name));
1471 }
1472
1473 fn pop_vnode(&mut self) {
1474 self.vnode_stack.pop();
1475 }
1476
1477 fn register_handler(
1478 &mut self,
1479 event_type: &str,
1480 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1481 ) {
1482 self.event_handlers
1483 .entry(event_type.to_string())
1484 .or_insert_with(Vec::new)
1485 .push(handler);
1486 }
1487
1488 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1489 GpuRenderer::load_svg(self, name, svg_data);
1490 }
1491
1492 fn draw_svg(&mut self, name: &str, rect: Rect) {
1493 GpuRenderer::draw_svg(self, name, rect, None, 0);
1494 }
1495 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, animation_time_offset: f32) {
1496 GpuRenderer::draw_svg_with_offset(self, name, rect, None, 0, animation_time_offset);
1497 }
1498
1499 fn draw_svg_with_order(&mut self, name: &str, rect: Rect, draw_order: i32) {
1502 GpuRenderer::draw_svg_with_order(self, name, rect, None, 0, 0.0, draw_order);
1503 }
1504
1505 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1506 let tree = self
1507 .svg
1508 .tree_cache
1509 .get(name)
1510 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1511 let config = cvkg_svg_serialize::SerializerConfig::default();
1512 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1513 serializer
1514 .serialize(tree)
1515 .map_err(|e| format!("SVG serialization failed: {}", e))
1516 }
1517
1518 fn apply_svg_filter(
1519 &mut self,
1520 name: &str,
1521 filter_id: &str,
1522 _region: Rect,
1523 ) -> Result<String, String> {
1524 let tree = self
1525 .svg
1526 .tree_cache
1527 .get(name)
1528 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1529 let _filter = Self::find_filter(tree, filter_id)
1530 .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1531 let config = cvkg_svg_serialize::SerializerConfig::default();
1532 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1533 serializer
1534 .serialize(tree)
1535 .map_err(|e| format!("SVG filter serialization failed: {}", e))
1536 }
1537
1538 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1539 self.measure_text_impl(text, size)
1540 }
1541
1542 fn draw_text(
1543 &mut self,
1544 text: &str,
1545 rect: &Rect,
1546 size: f32,
1547 color: [f32; 4],
1548 h_align: cvkg_core::TextHAlign,
1549 v_align: cvkg_core::TextVAlign,
1550 ) {
1551 self.draw_text_impl(text, rect, size, color, h_align, v_align);
1552 }
1553}
1554
1555impl GpuRenderer {
1558 pub fn clear_event_handlers(&mut self) {
1561 self.event_handlers.clear();
1562 }
1563
1564 pub fn clear_text_cache(&mut self) {
1566 self.clear_text_cache_impl();
1567 }
1568
1569 pub fn get_handlers(
1571 &self,
1572 event_type: &str,
1573 ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1574 self.event_handlers.get(event_type)
1575 }
1576
1577 pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1581 let m = self
1584 .transform_stack
1585 .last()
1586 .copied()
1587 .unwrap_or(glam::Mat3::IDENTITY);
1588 let t = [m.z_axis.x, m.z_axis.y];
1589 let a = m.x_axis.x;
1591 let b = m.x_axis.y;
1592 let c = m.y_axis.x;
1593 let d = m.y_axis.y;
1594 let sx = (a * a + b * b).sqrt();
1595 let sy = (c * c + d * d).sqrt();
1596 let rotation = b.atan2(a);
1597 let skew_x = (a * c + b * d) / (sx * sy); (t, [sx, sy], rotation, skew_x, 0.0)
1600 }
1601
1602 pub(crate) fn current_transform_3d(&self) -> glam::Mat4 {
1604 self.transform_stack_3d
1605 .last()
1606 .copied()
1607 .unwrap_or(glam::Mat4::IDENTITY)
1608 }
1609
1610 pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1611 self.stroke_path_impl(path, color, stroke_width);
1612 }
1613}
1614
1615#[cfg(test)]
1616mod transform_3d_tests {
1617 use cvkg_core::Transform3D;
1618 use glam::{Mat4, Quat, Vec3};
1619
1620 #[test]
1621 fn test_transform3d_to_matrix_translation() {
1622 let transform = Transform3D {
1623 position: Vec3::new(10.0, 20.0, 30.0),
1624 ..Default::default()
1625 };
1626 let m = transform.to_matrix();
1627 assert!((m.w_axis.x - 10.0).abs() < 0.001);
1628 assert!((m.w_axis.y - 20.0).abs() < 0.001);
1629 assert!((m.w_axis.z - 30.0).abs() < 0.001);
1630 }
1631
1632 #[test]
1633 fn test_transform3d_to_matrix_scale() {
1634 let transform = Transform3D {
1635 scale: Vec3::new(2.0, 3.0, 4.0),
1636 ..Default::default()
1637 };
1638 let m = transform.to_matrix();
1639 assert!((m.x_axis.x - 2.0).abs() < 0.001);
1640 assert!((m.y_axis.y - 3.0).abs() < 0.001);
1641 assert!((m.z_axis.z - 4.0).abs() < 0.001);
1642 }
1643
1644 #[test]
1645 fn test_transform3d_hierarchical_multiplication() {
1646 let parent = Transform3D {
1647 position: Vec3::new(10.0, 0.0, 0.0),
1648 ..Default::default()
1649 };
1650 let child = Transform3D {
1651 position: Vec3::new(5.0, 0.0, 0.0),
1652 ..Default::default()
1653 };
1654 let parent_m = parent.to_matrix();
1655 let child_m = child.to_matrix();
1656 let combined = parent_m * child_m;
1657 assert!((combined.w_axis.x - 15.0).abs() < 0.001);
1658 }
1659
1660 #[test]
1661 fn test_transform3d_rotation() {
1662 let transform = Transform3D {
1663 rotation: Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1664 ..Default::default()
1665 };
1666 let m = transform.to_matrix();
1667 assert!((m.x_axis.x).abs() < 0.001);
1669 assert!((m.x_axis.y - 1.0).abs() < 0.001);
1670 assert!((m.y_axis.x + 1.0).abs() < 0.001);
1671 assert!((m.y_axis.y).abs() < 0.001);
1672 }
1673}