feat: add logging, fix text color

This commit is contained in:
Tomáš Mládek 2026-01-24 22:39:09 +01:00
parent 3749bad021
commit abdfff92ca
3 changed files with 84 additions and 22 deletions

View file

@ -1,5 +1,5 @@
use crate::svg::{Renderable as _, SvgContent}; use crate::svg::{Renderable as _, SvgContent};
use crate::text_cache::{TextCache, RENDER_SCALE}; use crate::text_cache::{RENDER_SCALE, TextCache};
/// We derive Deserialize/Serialize so we can persist app state on shutdown. /// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
@ -58,8 +58,12 @@ impl Default for TemplateApp {
impl TemplateApp { impl TemplateApp {
/// Called once before the first frame. /// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self { pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
log::info!("Initializing application...");
log::debug!("Installing image loaders...");
egui_extras::install_image_loaders(&cc.egui_ctx); egui_extras::install_image_loaders(&cc.egui_ctx);
log::debug!("Loading app state...");
let mut app: Self = cc let mut app: Self = cc
.storage .storage
.and_then(|s| eframe::get_value(s, eframe::APP_KEY)) .and_then(|s| eframe::get_value(s, eframe::APP_KEY))
@ -67,18 +71,24 @@ impl TemplateApp {
// Load SVG content // Load SVG content
let svg_path = "../line-and-surface/content/intro.svg"; let svg_path = "../line-and-surface/content/intro.svg";
log::info!("Loading SVG from: {svg_path}");
let start = std::time::Instant::now();
match SvgContent::from_file(svg_path) { match SvgContent::from_file(svg_path) {
Ok(content) => { Ok(content) => {
let elapsed = start.elapsed();
log::info!( log::info!(
"Loaded SVG: {} video scrolls, {} audio areas, {} anchors, {} texts", "Loaded SVG in {:.2?}: {} video scrolls, {} audio areas, {} anchors, {} texts",
elapsed,
content.video_scrolls.len(), content.video_scrolls.len(),
content.audio_areas.len(), content.audio_areas.len(),
content.anchors.len(), content.anchors.len(),
content.texts.len() content.texts.len()
); );
if let Some((min_x, min_y, _, _)) = content.viewbox { if let Some((vb_x, vb_y, vb_w, vb_h)) = content.viewbox {
app.pan_x = -min_x; log::debug!("SVG viewbox: ({vb_x}, {vb_y}, {vb_w}, {vb_h})");
app.pan_y = -min_y; app.pan_x = -vb_x;
app.pan_y = -vb_y;
} }
app.svg_content = Some(content); app.svg_content = Some(content);
} }
@ -87,6 +97,7 @@ impl TemplateApp {
} }
} }
log::info!("Application initialized");
app app
} }
@ -258,7 +269,8 @@ impl eframe::App for TemplateApp {
// Video scrolls // Video scrolls
for vs in &content.video_scrolls { for vs in &content.video_scrolls {
let (x, y, w, h) = vs.bounds(); let (x, y, w, h) = vs.bounds();
let rect = egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); let rect =
egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h));
if rect.intersects(canvas_rect) { if rect.intersects(canvas_rect) {
painter.rect_filled(rect, 0.0, video_scroll_color); painter.rect_filled(rect, 0.0, video_scroll_color);
rendered_count += 1; rendered_count += 1;
@ -268,7 +280,8 @@ impl eframe::App for TemplateApp {
// Audio areas // Audio areas
for aa in &content.audio_areas { for aa in &content.audio_areas {
let (x, y, w, h) = aa.bounds(); let (x, y, w, h) = aa.bounds();
let rect = egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); let rect =
egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h));
if rect.intersects(canvas_rect) { if rect.intersects(canvas_rect) {
painter.rect_filled(rect, (w.min(h) * zoom) / 2.0, audio_area_color); painter.rect_filled(rect, (w.min(h) * zoom) / 2.0, audio_area_color);
rendered_count += 1; rendered_count += 1;
@ -278,7 +291,8 @@ impl eframe::App for TemplateApp {
// Anchors // Anchors
for anchor in &content.anchors { for anchor in &content.anchors {
let (x, y, w, h) = anchor.bounds(); let (x, y, w, h) = anchor.bounds();
let rect = egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); let rect =
egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h));
if rect.intersects(canvas_rect) { if rect.intersects(canvas_rect) {
painter.rect_filled(rect, 0.0, anchor_color); painter.rect_filled(rect, 0.0, anchor_color);
rendered_count += 1; rendered_count += 1;
@ -288,7 +302,8 @@ impl eframe::App for TemplateApp {
// Text elements // Text elements
for text_elem in &content.texts { for text_elem in &content.texts {
let (x, y, w, h) = text_elem.bounds(); let (x, y, w, h) = text_elem.bounds();
let rect = egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); let rect =
egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h));
if !rect.intersects(canvas_rect) { if !rect.intersects(canvas_rect) {
continue; continue;
@ -301,7 +316,8 @@ impl eframe::App for TemplateApp {
continue; continue;
} }
let cached = text_cache.get_or_create(ctx, &line.content, text_elem.font_size); let cached =
text_cache.get_or_create(ctx, &line.content, text_elem.font_size);
let scale_factor = zoom / RENDER_SCALE; let scale_factor = zoom / RENDER_SCALE;
let display_width = cached.width as f32 * scale_factor; let display_width = cached.width as f32 * scale_factor;
let display_height = cached.height as f32 * scale_factor; let display_height = cached.height as f32 * scale_factor;
@ -311,8 +327,14 @@ impl eframe::App for TemplateApp {
painter.image( painter.image(
cached.texture.id(), cached.texture.id(),
egui::Rect::from_min_size(pos, egui::vec2(display_width, display_height)), egui::Rect::from_min_size(
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), pos,
egui::vec2(display_width, display_height),
),
egui::Rect::from_min_max(
egui::pos2(0.0, 0.0),
egui::pos2(1.0, 1.0),
),
text_color, // Tint the white texture with desired color text_color, // Tint the white texture with desired color
); );
} }
@ -357,7 +379,15 @@ impl eframe::App for TemplateApp {
+ content.texts.len(); + content.texts.len();
ui.label(format!("Total elements: {total}")); ui.label(format!("Total elements: {total}"));
ui.label(format!("Rendered: {rendered_count}")); ui.label(format!("Rendered: {rendered_count}"));
ui.label(format!("Culled: {}", total.saturating_sub(rendered_count as usize))); ui.label(format!(
"Culled: {}",
total.saturating_sub(rendered_count as usize)
));
}
if let Some(ref cache) = self.text_cache {
ui.separator();
ui.label(format!("Text cache: {} entries", cache.cache_size()));
} }
}); });
} }

View file

@ -1,7 +1,7 @@
//! SVG parsing module for extracting special elements from SVG files. //! SVG parsing module for extracting special elements from SVG files.
use quick_xml::events::Event;
use quick_xml::Reader; use quick_xml::Reader;
use quick_xml::events::Event;
use std::fs; use std::fs;
/// Trait for elements that can be rendered with a bounding box. /// Trait for elements that can be rendered with a bounding box.
@ -95,7 +95,11 @@ impl Renderable for TextElement {
.iter() .iter()
.map(|l| l.x + l.content.len() as f32 * self.font_size * 0.6) .map(|l| l.x + l.content.len() as f32 * self.font_size * 0.6)
.fold(f32::NEG_INFINITY, f32::max); .fold(f32::NEG_INFINITY, f32::max);
let max_y = self.lines.iter().map(|l| l.y).fold(f32::NEG_INFINITY, f32::max); let max_y = self
.lines
.iter()
.map(|l| l.y)
.fold(f32::NEG_INFINITY, f32::max);
(min_x, min_y, max_x - min_x, max_y - min_y) (min_x, min_y, max_x - min_x, max_y - min_y)
} }
@ -136,13 +140,16 @@ enum PendingElement {
impl SvgContent { impl SvgContent {
/// Parse an SVG file and extract special elements. /// Parse an SVG file and extract special elements.
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
log::debug!("Reading SVG file: {path}");
let content = fs::read_to_string(path)?; let content = fs::read_to_string(path)?;
log::debug!("SVG file size: {} bytes", content.len());
Self::parse(&content) Self::parse(&content)
} }
/// Parse SVG content from a string. /// Parse SVG content from a string.
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]
pub fn parse(content: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn parse(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
log::debug!("Parsing SVG content ({} bytes)...", content.len());
let mut reader = Reader::from_str(content); let mut reader = Reader::from_str(content);
reader.config_mut().trim_text(true); reader.config_mut().trim_text(true);
@ -286,7 +293,9 @@ impl SvgContent {
// Parse font-size from style attribute // Parse font-size from style attribute
if let Some(size_start) = value.find("font-size:") { if let Some(size_start) = value.find("font-size:") {
let size_str = &value[size_start + 10..]; let size_str = &value[size_start + 10..];
if let Some(size_end) = size_str.find(|c: char| !c.is_numeric() && c != '.') { if let Some(size_end) =
size_str.find(|c: char| !c.is_numeric() && c != '.')
{
if let Ok(size) = size_str[..size_end].parse::<f32>() { if let Ok(size) = size_str[..size_end].parse::<f32>() {
text_font_size = size; text_font_size = size;
} }
@ -444,6 +453,14 @@ impl SvgContent {
buf.clear(); buf.clear();
} }
log::debug!(
"SVG parsing complete: {} video scrolls, {} audio areas, {} anchors, {} texts",
svg_content.video_scrolls.len(),
svg_content.audio_areas.len(),
svg_content.anchors.len(),
svg_content.texts.len()
);
Ok(svg_content) Ok(svg_content)
} }
} }

View file

@ -44,8 +44,14 @@ impl TextCache {
/// ///
/// Text is rendered in white - apply color as a tint when drawing. /// Text is rendered in white - apply color as a tint when drawing.
pub fn new() -> Self { pub fn new() -> Self {
log::debug!("Initializing text cache...");
let font_data: &'static [u8] = include_bytes!("../assets/NotoSans-Regular.ttf"); let font_data: &'static [u8] = include_bytes!("../assets/NotoSans-Regular.ttf");
let font = FontRef::try_from_slice(font_data).expect("embedded font should be valid"); let font = FontRef::try_from_slice(font_data).expect("embedded font should be valid");
log::info!(
"Text cache initialized (font: {} bytes, render scale: {}x)",
font_data.len(),
RENDER_SCALE
);
Self { Self {
font, font,
@ -53,6 +59,11 @@ impl TextCache {
} }
} }
/// Returns the number of cached text textures.
pub fn cache_size(&self) -> usize {
self.cache.len()
}
/// Get or create a cached texture for the given text. /// Get or create a cached texture for the given text.
/// ///
/// The texture is rendered at `RENDER_SCALE` times the nominal font size. /// The texture is rendered at `RENDER_SCALE` times the nominal font size.
@ -115,7 +126,15 @@ impl TextCache {
} }
// Render glyphs to pixel buffer // Render glyphs to pixel buffer
let pixels = Self::render_glyphs(text, &scaled_font, scale, ascent, padding, img_width, img_height); let pixels = Self::render_glyphs(
text,
&scaled_font,
scale,
ascent,
padding,
img_width,
img_height,
);
// Create egui texture // Create egui texture
let image = ColorImage { let image = ColorImage {
@ -223,11 +242,7 @@ impl TextCache {
source_size: egui::Vec2::new(1.0, 1.0), source_size: egui::Vec2::new(1.0, 1.0),
}; };
let texture = ctx.load_texture( let texture = ctx.load_texture(format!("text_empty_{size_key}"), image, TEXTURE_FILTER);
format!("text_empty_{size_key}"),
image,
TEXTURE_FILTER,
);
CachedText { CachedText {
texture, texture,