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::text_cache::{TextCache, RENDER_SCALE};
use crate::text_cache::{RENDER_SCALE, TextCache};
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
@ -58,8 +58,12 @@ impl Default for TemplateApp {
impl TemplateApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
log::info!("Initializing application...");
log::debug!("Installing image loaders...");
egui_extras::install_image_loaders(&cc.egui_ctx);
log::debug!("Loading app state...");
let mut app: Self = cc
.storage
.and_then(|s| eframe::get_value(s, eframe::APP_KEY))
@ -67,18 +71,24 @@ impl TemplateApp {
// Load SVG content
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) {
Ok(content) => {
let elapsed = start.elapsed();
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.audio_areas.len(),
content.anchors.len(),
content.texts.len()
);
if let Some((min_x, min_y, _, _)) = content.viewbox {
app.pan_x = -min_x;
app.pan_y = -min_y;
if let Some((vb_x, vb_y, vb_w, vb_h)) = content.viewbox {
log::debug!("SVG viewbox: ({vb_x}, {vb_y}, {vb_w}, {vb_h})");
app.pan_x = -vb_x;
app.pan_y = -vb_y;
}
app.svg_content = Some(content);
}
@ -87,6 +97,7 @@ impl TemplateApp {
}
}
log::info!("Application initialized");
app
}
@ -258,7 +269,8 @@ impl eframe::App for TemplateApp {
// Video scrolls
for vs in &content.video_scrolls {
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) {
painter.rect_filled(rect, 0.0, video_scroll_color);
rendered_count += 1;
@ -268,7 +280,8 @@ impl eframe::App for TemplateApp {
// Audio areas
for aa in &content.audio_areas {
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) {
painter.rect_filled(rect, (w.min(h) * zoom) / 2.0, audio_area_color);
rendered_count += 1;
@ -278,7 +291,8 @@ impl eframe::App for TemplateApp {
// Anchors
for anchor in &content.anchors {
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) {
painter.rect_filled(rect, 0.0, anchor_color);
rendered_count += 1;
@ -288,7 +302,8 @@ impl eframe::App for TemplateApp {
// Text elements
for text_elem in &content.texts {
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) {
continue;
@ -301,7 +316,8 @@ impl eframe::App for TemplateApp {
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 display_width = cached.width as f32 * scale_factor;
let display_height = cached.height as f32 * scale_factor;
@ -311,8 +327,14 @@ impl eframe::App for TemplateApp {
painter.image(
cached.texture.id(),
egui::Rect::from_min_size(pos, egui::vec2(display_width, display_height)),
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
egui::Rect::from_min_size(
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
);
}
@ -357,7 +379,15 @@ impl eframe::App for TemplateApp {
+ content.texts.len();
ui.label(format!("Total elements: {total}"));
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.
use quick_xml::events::Event;
use quick_xml::Reader;
use quick_xml::events::Event;
use std::fs;
/// Trait for elements that can be rendered with a bounding box.
@ -95,7 +95,11 @@ impl Renderable for TextElement {
.iter()
.map(|l| l.x + l.content.len() as f32 * self.font_size * 0.6)
.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)
}
@ -136,13 +140,16 @@ enum PendingElement {
impl SvgContent {
/// Parse an SVG file and extract special elements.
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)?;
log::debug!("SVG file size: {} bytes", content.len());
Self::parse(&content)
}
/// Parse SVG content from a string.
#[expect(clippy::too_many_lines)]
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);
reader.config_mut().trim_text(true);
@ -286,7 +293,9 @@ impl SvgContent {
// Parse font-size from style attribute
if let Some(size_start) = value.find("font-size:") {
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>() {
text_font_size = size;
}
@ -444,6 +453,14 @@ impl SvgContent {
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)
}
}

View file

@ -44,8 +44,14 @@ impl TextCache {
///
/// Text is rendered in white - apply color as a tint when drawing.
pub fn new() -> Self {
log::debug!("Initializing text cache...");
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");
log::info!(
"Text cache initialized (font: {} bytes, render scale: {}x)",
font_data.len(),
RENDER_SCALE
);
Self {
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.
///
/// The texture is rendered at `RENDER_SCALE` times the nominal font size.
@ -115,7 +126,15 @@ impl TextCache {
}
// 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
let image = ColorImage {
@ -223,11 +242,7 @@ impl TextCache {
source_size: egui::Vec2::new(1.0, 1.0),
};
let texture = ctx.load_texture(
format!("text_empty_{size_key}"),
image,
TEXTURE_FILTER,
);
let texture = ctx.load_texture(format!("text_empty_{size_key}"), image, TEXTURE_FILTER);
CachedText {
texture,