cvkg_render_gpu/surtr_util.rs
1//! Atlas loading and text shaping utilities.
2use crate::renderer::SurtrRenderer;
3use cvkg_core::{Rect, Renderer};
4
5impl SurtrRenderer {
6 /// load_image_to_heim — Packs a raw asset into the Mega-Heim.
7 /// This is used for common icons to enable aggressive batching (1 draw call).
8 pub fn load_image_to_heim(&mut self, name: &str, data: &[u8]) {
9 if self.image_uv_registry.contains(name) {
10 return;
11 }
12 let img_result = image::load_from_memory(data);
13 let img = match img_result {
14 Ok(img) => img.to_rgba8(),
15 Err(e) => {
16 log::error!("Failed to load image {} to heim: {}", name, e);
17 return;
18 }
19 };
20 let (width, height) = img.dimensions();
21
22 // Pack into heim
23 if let Some((x, y)) = self.heim_packer.pack(width, height) {
24 let uv_rect = Rect {
25 x: x as f32 / 4096.0,
26 y: y as f32 / 4096.0,
27 width: width as f32 / 4096.0,
28 height: height as f32 / 4096.0,
29 };
30
31 // Upload to GPU
32 self.queue.write_texture(
33 wgpu::TexelCopyTextureInfo {
34 texture: &self.mega_heim_tex,
35 mip_level: 0,
36 origin: wgpu::Origin3d { x, y, z: 0 },
37 aspect: wgpu::TextureAspect::All,
38 },
39 &img,
40 wgpu::TexelCopyBufferLayout {
41 offset: 0,
42 bytes_per_row: Some(4 * width),
43 rows_per_image: Some(height),
44 },
45 wgpu::Extent3d {
46 width,
47 height,
48 depth_or_array_layers: 1,
49 },
50 );
51
52 self.image_uv_registry.put(name.to_string(), uv_rect);
53 // Index 0 = mega-heim texture (stored in texture_views[0])
54 self.texture_registry.put(name.to_string(), 0);
55 log::debug!("[Surtr] Packed '{}' into Mega-Heim at ({}, {})", name, x, y);
56 } else {
57 log::warn!(
58 "HEIM_FULL: Failed to pack '{}' into Mega-Heim. Falling back to Texture Array.",
59 name
60 );
61 self.load_image(name, data);
62 }
63 }
64
65 /// Shapes a text string using a predefined system font stack.
66 ///
67 /// # Contract
68 /// Evaluates text shaping with fallbacks: queries "SF Pro Text", "SF Pro", "Inter",
69 /// "Helvetica Neue", "Helvetica", "Arial", and defaults back to "sans-serif".
70 /// This ensures visual typographic consistency across platforms where specific
71 /// branding faces may or may not be installed.
72 /// Shapes a text string using a default font stack.
73 ///
74 /// # Contract
75 /// Resolves standard font families in order of system availability. Falls back from
76 /// common system sans-serif aliases, to platform-specific sans-serif faces, and finally
77 /// to the embedded "Jupiteroid" font as a last resort.
78 /// Shapes a text string using a predefined system font stack.
79 ///
80 /// # Contract
81 /// Evaluates text shaping with fallbacks: queries "SF Pro Text", "SF Pro", "Inter",
82 /// "Helvetica Neue", "Helvetica", "Arial", and defaults back to "sans-serif".
83 /// This ensures visual typographic consistency across platforms where specific
84 /// branding faces may or may not be installed.
85 ///
86 /// The shaped text result is cached in `shaped_text_cache` by content and size.
87 /// This layout cache guarantees sub-millisecond execution times for subsequent
88 /// lookups, bypassing expensive font config fallback queries on repeating frames.
89 pub(crate) fn shape_text_with_stack(
90 &mut self,
91 text: &str,
92 size: f32,
93 ) -> cvkg_runic_text::ShapedText {
94 let cache_key = (text.to_string(), (size * 100.0) as u32);
95 if let Some(shaped) = self.shaped_text_cache.get(&cache_key) {
96 return shaped.clone();
97 }
98
99 let mut style = cvkg_runic_text::TextStyle::new("Jupiteroid", size);
100 style.fallback_families = vec![
101 "sans-serif".to_string(),
102 // Linux-native (fontconfig standard aliases + common packages)
103 "DejaVu Sans".to_string(),
104 "Cantarell".to_string(),
105 "Liberation Sans".to_string(),
106 "Noto Sans".to_string(),
107 "Adwaita Sans".to_string(),
108 // macOS / Windows
109 "SF Pro".to_string(),
110 "SF Pro Text".to_string(),
111 "Inter".to_string(),
112 "Helvetica Neue".to_string(),
113 "Helvetica".to_string(),
114 "Arial".to_string(),
115 ];
116 style.render_mode = cvkg_runic_text::RenderMode::Subpixel;
117 let spans = vec![cvkg_runic_text::TextSpan::new(text, style)];
118 let shaped = self
119 .text_engine
120 .shape_layout(
121 &spans,
122 None,
123 cvkg_runic_text::TextAlign::Start,
124 cvkg_runic_text::TextOverflow::WordWrap,
125 )
126 .unwrap_or_else(|_| cvkg_runic_text::ShapedText {
127 glyphs: Vec::new(),
128 lines: Vec::new(),
129 width: 0.0,
130 height: 0.0,
131 text: text.to_string(),
132 spans: Vec::new(),
133 has_rtl: false,
134 ascent: 0.0,
135 descent: 0.0,
136 line_gap: 0.0,
137 grapheme_boundaries: vec![],
138 });
139
140 self.shaped_text_cache.insert(cache_key, shaped.clone());
141 shaped
142 }
143}