Skip to main content

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}