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