feat: add logging, fix text color
This commit is contained in:
parent
3749bad021
commit
abdfff92ca
3 changed files with 84 additions and 22 deletions
56
src/app.rs
56
src/app.rs
|
|
@ -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()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
23
src/svg.rs
23
src/svg.rs
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue