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 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}