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::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()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/svg.rs
23
src/svg.rs
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue