From afd0794104228cd20e177f41dc68e9ae7ecaabda Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Tue, 16 Dec 2025 12:07:43 +0100 Subject: Performance improvements, prep for publish. --- src/renderer.rs | 791 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 441 insertions(+), 350 deletions(-) (limited to 'src/renderer.rs') diff --git a/src/renderer.rs b/src/renderer.rs index 74f4708..a673bac 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,65 +1,108 @@ +use std::f64::consts::PI; +use std::fs::read_to_string; use std::io::Cursor; -use wmap_parser::Map; + +use cairo::{ + Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface, +}; +use geo::{ConcaveHull, LineString, Point}; +use rustc_hash::FxHashMap; +use tempfile::NamedTempFile; +use wmap_parser::{Map, Stage}; use crate::configuration::Configuration; +use crate::error::RenderError; +use crate::patterns::{SHADOW_GRID, SHINGLES, STITCH, WICKER, create_pattern}; +use crate::shapes::draw_vertex; +use crate::smart_positioning::{ + PositioningDimensions, build_spatial_grid, calculate_optimal_label_position_cached, + collect_all_lines, collect_note_rects, +}; use crate::stage_type::StageType; -use crate::utils::{percent_to_pixel, set_color}; -use crate::RenderError; -use cairo::{Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface}; - -/// Renders a map to PNG format +use crate::utils::percent_to_pixel; + +/// Renders a Wardley map to PNG format +/// +/// # Arguments +/// +/// * `map` - The parsed Wardley map +/// * `stage_type` - The stage type to use for axis labels +/// * `config` - Configuration options +/// +/// # Returns +/// +/// A vector of bytes containing the PNG image data pub fn render_to_png( map: &Map, stage_type: StageType, - config: &Configuration, + configuration: &Configuration, ) -> Result, RenderError> { - let total_width = (config.theme.sizes.map_width + config.theme.sizes.padding * 2.0) as i32; - let total_height = (config.theme.sizes.map_height - + config.theme.sizes.padding - + config.theme.sizes.bottom_padding) as i32; + let total_width = + (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32; + let total_height = (configuration.theme.sizes.map_height + + configuration.theme.sizes.padding + + configuration.theme.sizes.bottom_padding) as i32; let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?; - let ctx = Context::new(&surface)?; + let context = Context::new(&surface)?; - render(&ctx, map, stage_type, config)?; + render(&context, map, stage_type, configuration)?; let mut buffer = Cursor::new(Vec::new()); surface.write_to_png(&mut buffer)?; Ok(buffer.into_inner()) } -/// Renders a map to the context +/// Renders a Wardley map to a cairo context. +/// +/// # Arguments +/// +/// * `map` - The parsed Wardley map +/// * `stage_type` - The stage type to use for axis labels +/// * `config` - Configuration options +/// +/// # Returns +/// +/// A string containing the SVG data pub fn render_to_surface( map: &Map, stage_type: StageType, - config: &Configuration, + configuration: &Configuration, ) -> Result { - let total_width = (config.theme.sizes.map_width + config.theme.sizes.padding * 2.0) as i32; - let total_height = (config.theme.sizes.map_height - + config.theme.sizes.padding - + config.theme.sizes.bottom_padding) as i32; + let total_width = + (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32; + let total_height = (configuration.theme.sizes.map_height + + configuration.theme.sizes.padding + + configuration.theme.sizes.bottom_padding) as i32; let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?; - let ctx = Context::new(&surface)?; + let context = Context::new(&surface)?; - render(&ctx, map, stage_type, config)?; + render(&context, map, stage_type, configuration)?; Ok(surface) } -/// Renders a map to SVG format +/// Renders a Wardley map to SVG format +/// +/// # Arguments +/// +/// * `map` - The parsed Wardley map +/// * `stage_type` - The stage type to use for axis labels +/// * `config` - Configuration options +/// +/// # Returns +/// +/// A string containing the SVG data pub fn render_to_svg( map: &Map, stage_type: StageType, - config: &Configuration, + configuration: &Configuration, ) -> Result { - use std::fs; - use tempfile::NamedTempFile; - - let total_width = config.theme.sizes.map_width + config.theme.sizes.padding * 2.0; - let total_height = config.theme.sizes.map_height - + config.theme.sizes.padding - + config.theme.sizes.bottom_padding; + let total_width = configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0; + let total_height = configuration.theme.sizes.map_height + + configuration.theme.sizes.padding + + configuration.theme.sizes.bottom_padding; let temp_file = NamedTempFile::new()?; let path = temp_file @@ -68,201 +111,204 @@ pub fn render_to_svg( .ok_or_else(|| RenderError::InvalidConfig("Failed to create temp file path".to_string()))?; let surface = SvgSurface::new(total_width, total_height, Some(path))?; - let ctx = Context::new(&surface)?; + let context = Context::new(&surface)?; - render(&ctx, map, stage_type, config)?; + render(&context, map, stage_type, configuration)?; - drop(ctx); + drop(context); surface.finish(); - let svg_content = fs::read_to_string(path)?; + let svg_content = read_to_string(path)?; Ok(svg_content) } /// Core rendering function fn render( - ctx: &Context, + context: &Context, map: &Map, stage_type: StageType, - config: &Configuration, + configuration: &Configuration, ) -> Result<(), RenderError> { - use std::collections::HashMap; - // Apply padding transform - ctx.save()?; - ctx.translate(config.theme.sizes.padding, config.theme.sizes.padding); - - // Build component map once and reuse it - let component_map: HashMap = map.components - .iter() - .map(|component| { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - (component.label.to_lowercase(), (x, y)) - }) - .collect(); + context.save()?; + context.translate( + configuration.theme.sizes.padding, + configuration.theme.sizes.padding, + ); + + // Build component map once and reuse it (pre-allocate capacity for better performance) + // Use FxHashMap for faster hashing (we don't need cryptographic security) + let mut component_map: FxHashMap = + FxHashMap::with_capacity_and_hasher(map.components.len(), Default::default()); + for component in &map.components { + let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); + let y = percent_to_pixel( + component.coordinates.1, + configuration.theme.sizes.map_height, + ); + component_map.insert(component.label.to_lowercase(), (x, y)); + } // Render in order (back to front) - draw_background(ctx, config)?; + draw_background(context, configuration)?; - if config.options.show_background { - draw_stage_patterns(ctx, stage_type, map, config)?; + if configuration.options.show_background { + draw_stage_patterns(context, stage_type, map, configuration)?; } - draw_axes(ctx, stage_type, map, config)?; - draw_stage_dividers(ctx, map, config)?; - draw_dependencies(ctx, map, config, &component_map)?; - draw_groups(ctx, map, config, &component_map)?; - draw_vertices(ctx, map, config)?; - draw_vertex_labels(ctx, map, config, &component_map)?; - draw_inertias(ctx, map, config, &component_map)?; - draw_evolutions(ctx, map, config, &component_map)?; - draw_notes(ctx, map, config)?; - - ctx.restore()?; + draw_axes(context, stage_type, map, configuration)?; + draw_stage_dividers(context, map, configuration)?; + draw_dependencies(context, map, configuration, &component_map)?; + draw_groups(context, map, configuration, &component_map)?; + draw_vertices(context, map, configuration)?; + draw_vertex_labels(context, map, configuration, &component_map)?; + draw_inertias(context, map, configuration, &component_map)?; + draw_evolutions(context, map, configuration, &component_map)?; + draw_notes(context, map, configuration)?; + + context.restore()?; Ok(()) } -fn draw_background(ctx: &Context, config: &Configuration) -> Result<(), RenderError> { - ctx.save()?; - set_color(ctx, &config.theme.colors.background); - ctx.paint()?; - ctx.restore()?; +fn draw_background(context: &Context, configuration: &Configuration) -> Result<(), RenderError> { + context.save()?; + let (r, g, b) = configuration.theme.colors.background; + context.set_source_rgb(r, g, b); + context.paint()?; + context.restore()?; Ok(()) } fn draw_stage_patterns( - ctx: &Context, + context: &Context, _stage_type: StageType, map: &Map, - config: &Configuration, + configuration: &Configuration, ) -> Result<(), RenderError> { - use crate::patterns::{create_pattern, SHADOW_GRID, SHINGLES, STITCH, WICKER}; - - let stages = get_stage_positions(map, config); - let patterns = [ - create_pattern( - &STITCH, - &config.theme.colors.stage_foreground, - &config.theme.colors.stage_background, - )?, - create_pattern( - &SHINGLES, - &config.theme.colors.stage_foreground, - &config.theme.colors.stage_background, - )?, - create_pattern( - &SHADOW_GRID, - &config.theme.colors.stage_foreground, - &config.theme.colors.stage_background, - )?, - create_pattern( - &WICKER, - &config.theme.colors.stage_foreground, - &config.theme.colors.stage_background, - )?, - ]; - - ctx.save()?; + let stages = get_stage_positions(map, configuration); + + // Create patterns once (they're small and fast to create) + let pattern0 = create_pattern( + &STITCH, + configuration.theme.colors.stage_foreground, + configuration.theme.colors.stage_background, + )?; + let pattern1 = create_pattern( + &SHINGLES, + configuration.theme.colors.stage_foreground, + configuration.theme.colors.stage_background, + )?; + let pattern2 = create_pattern( + &SHADOW_GRID, + configuration.theme.colors.stage_foreground, + configuration.theme.colors.stage_background, + )?; + let pattern3 = create_pattern( + &WICKER, + configuration.theme.colors.stage_foreground, + configuration.theme.colors.stage_background, + )?; + + context.save()?; // Draw pattern for stage 0 to stage 1 - ctx.rectangle(0.0, 0.0, stages[0], config.theme.sizes.map_height); - ctx.set_source(&patterns[0])?; - ctx.fill()?; + context.rectangle(0.0, 0.0, stages[0], configuration.theme.sizes.map_height); + context.set_source(&pattern0)?; + context.fill()?; // Draw pattern for stage 1 to stage 2 - ctx.rectangle( + context.rectangle( stages[0], 0.0, stages[1] - stages[0], - config.theme.sizes.map_height, + configuration.theme.sizes.map_height, ); - ctx.set_source(&patterns[1])?; - ctx.fill()?; + context.set_source(&pattern1)?; + context.fill()?; // Draw pattern for stage 2 to stage 3 - ctx.rectangle( + context.rectangle( stages[1], 0.0, stages[2] - stages[1], - config.theme.sizes.map_height, + configuration.theme.sizes.map_height, ); - ctx.set_source(&patterns[2])?; - ctx.fill()?; + context.set_source(&pattern2)?; + context.fill()?; // Draw pattern for stage 3 to end - ctx.rectangle( + context.rectangle( stages[2], 0.0, - config.theme.sizes.map_width - stages[2], - config.theme.sizes.map_height, + configuration.theme.sizes.map_width - stages[2], + configuration.theme.sizes.map_height, ); - ctx.set_source(&patterns[3])?; - ctx.fill()?; + context.set_source(&pattern3)?; + context.fill()?; - ctx.restore()?; + context.restore()?; Ok(()) } fn draw_axes( - ctx: &Context, + context: &Context, stage_type: StageType, map: &Map, - config: &Configuration, + configuration: &Configuration, ) -> Result<(), RenderError> { - use std::f64::consts::PI; + let sizes = &configuration.theme.sizes; + let fonts = &configuration.theme.fonts; - let sizes = &config.theme.sizes; - let fonts = &config.theme.fonts; - - ctx.save()?; - set_color(ctx, &config.theme.colors.axis); - ctx.set_line_width(sizes.line_width * 2.0); + context.save()?; + let (r, g, b) = configuration.theme.colors.axis; + context.set_source_rgb(r, g, b); + context.set_line_width(sizes.line_width * 2.0); // Y-axis and X-axis - ctx.move_to(0.0, 0.0); - ctx.line_to(0.0, sizes.map_height); - ctx.line_to(sizes.map_width, sizes.map_height); - ctx.stroke()?; + context.move_to(0.0, 0.0); + context.line_to(0.0, sizes.map_height); + context.line_to(sizes.map_width, sizes.map_height); + context.stroke()?; // Text setup - set_color(ctx, &config.theme.colors.label); - ctx.select_font_face( - "sans-serif", + let (r, g, b) = configuration.theme.colors.label; + context.set_source_rgb(r, g, b); + context.select_font_face( + &configuration.theme.fonts.face, FontSlant::Normal, FontWeight::Normal, ); - ctx.set_font_size(fonts.axis_label); - ctx.set_line_width(1.0); + context.set_font_size(fonts.axis_label); + context.set_line_width(1.0); // Y-axis labels (rotated -90 degrees) - ctx.save()?; - ctx.translate(-20.0, 45.0); - ctx.rotate(-PI / 2.0); - ctx.move_to(0.0, 0.0); - ctx.show_text("Visible")?; - ctx.restore()?; - - ctx.save()?; - ctx.translate(-20.0, sizes.map_height); - ctx.rotate(-PI / 2.0); - ctx.move_to(0.0, 0.0); - ctx.show_text("Invisible")?; - ctx.restore()?; + context.save()?; + context.translate(-20.0, 45.0); + context.rotate(-PI / 2.0); + context.move_to(0.0, 0.0); + context.show_text("Visible")?; + context.restore()?; + + context.save()?; + context.translate(-20.0, sizes.map_height); + context.rotate(-PI / 2.0); + context.move_to(0.0, 0.0); + context.show_text("Invisible")?; + context.restore()?; // X-axis labels (above the map, at negative Y, offset by half vertex height) let stage_height = sizes.stage_height; let text_offset_y = -stage_height / 4.0 + sizes.vertex_size / 2.0; - ctx.move_to(0.0, text_offset_y); - ctx.show_text("Uncharted")?; + context.move_to(0.0, text_offset_y); + context.show_text("Uncharted")?; - let text_ext = ctx.text_extents("Industrialised")?; - ctx.move_to(sizes.map_width - text_ext.width(), text_offset_y); - ctx.show_text("Industrialised")?; + let text_ext = context.text_extents("Industrialised")?; + context.move_to(sizes.map_width - text_ext.width(), text_offset_y); + context.show_text("Industrialised")?; // Stage labels (below the map, offset by half vertex height, with text wrapping) let stage_labels = stage_type.stages(); - let stages = get_stage_positions(map, config); + let stages = get_stage_positions(map, configuration); let padding = 5.0; // kAxisLabelPadding let stage_label_y = sizes.map_height + padding + sizes.vertex_size / 2.0; let line_height = fonts.axis_label * 1.2; // 120% line height @@ -285,55 +331,55 @@ fn draw_axes( _ => continue, }; - let wrapped_lines = wrap_text(ctx, label, stage_widths[i])?; + let wrapped_lines = wrap_text(context, label, stage_widths[i])?; for (line_index, line) in wrapped_lines.iter().enumerate() { let y = stage_label_y + line_index as f64 * line_height; - ctx.move_to(x, y); - ctx.show_text(line)?; + context.move_to(x, y); + context.show_text(line)?; } } - ctx.restore()?; + context.restore()?; Ok(()) } fn draw_stage_dividers( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, + configuration: &Configuration, ) -> Result<(), RenderError> { - let stages = get_stage_positions(map, config); + let stages = get_stage_positions(map, configuration); - ctx.save()?; - set_color(ctx, &config.theme.colors.axis); - ctx.set_line_width(config.theme.sizes.line_width * 2.0); - ctx.set_dash(&[10.0, 18.0], 0.0); + context.save()?; + let (r, g, b) = configuration.theme.colors.axis; + context.set_source_rgb(r, g, b); + context.set_line_width(configuration.theme.sizes.line_width * 2.0); + context.set_dash(&[10.0, 18.0], 0.0); for &stage_x in &stages { - ctx.move_to(stage_x, 0.0); - ctx.line_to(stage_x, config.theme.sizes.map_height); + context.move_to(stage_x, 0.0); + context.line_to(stage_x, configuration.theme.sizes.map_height); } - ctx.stroke()?; - ctx.restore()?; + context.stroke()?; + context.restore()?; Ok(()) } fn draw_dependencies( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, - component_map: &std::collections::HashMap, + configuration: &Configuration, + component_map: &rustc_hash::FxHashMap, ) -> Result<(), RenderError> { - use std::f64::consts::PI; - - ctx.save()?; - set_color(ctx, &config.theme.colors.vertex); - ctx.set_line_width(config.theme.sizes.line_width); + context.save()?; + let (r, g, b) = configuration.theme.colors.vertex; + context.set_source_rgb(r, g, b); + context.set_line_width(configuration.theme.sizes.line_width); - let half_width = config.theme.sizes.vertex_size / 2.0; - let half_height = config.theme.sizes.vertex_size / 2.0; + let half_width = configuration.theme.sizes.vertex_size / 2.0; + let half_height = configuration.theme.sizes.vertex_size / 2.0; let arrow_angle = PI / 4.0; // 45 degrees for dependency in &map.dependencies { @@ -362,21 +408,21 @@ fn draw_dependencies( let offset_dest_y = dest_y - half_height * sine; // Draw line - ctx.move_to(offset_origin_x, offset_origin_y); - ctx.line_to(offset_dest_x, offset_dest_y); + context.move_to(offset_origin_x, offset_origin_y); + context.line_to(offset_dest_x, offset_dest_y); // Draw arrowhead if directed if dependency.is_directed { let upper_angle = angle - arrow_angle; let lower_angle = angle + arrow_angle; - let arrowhead_size = config.theme.sizes.arrowhead_size; + let arrowhead_size = configuration.theme.sizes.arrowhead_size; - ctx.move_to( + context.move_to( offset_dest_x - arrowhead_size * upper_angle.cos(), offset_dest_y - arrowhead_size * upper_angle.sin(), ); - ctx.line_to(offset_dest_x, offset_dest_y); - ctx.line_to( + context.line_to(offset_dest_x, offset_dest_y); + context.line_to( offset_dest_x - arrowhead_size * lower_angle.cos(), offset_dest_y - arrowhead_size * lower_angle.sin(), ); @@ -384,35 +430,34 @@ fn draw_dependencies( } // Batch stroke all paths at once - ctx.stroke()?; + context.stroke()?; - ctx.restore()?; + context.restore()?; Ok(()) } fn draw_groups( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, - component_map: &std::collections::HashMap, + configuration: &Configuration, + component_map: &rustc_hash::FxHashMap, ) -> Result<(), RenderError> { - use geo::{ConcaveHull, LineString, Point}; - use std::f64::consts::PI; - if map.groups.is_empty() { return Ok(()); } // Create ONE offscreen surface for ALL groups for opacity handling - let width = (config.theme.sizes.map_width + config.theme.sizes.vertex_size) as i32; - let height = (config.theme.sizes.map_height + config.theme.sizes.vertex_size) as i32; + let width = + (configuration.theme.sizes.map_width + configuration.theme.sizes.vertex_size) as i32; + let height = + (configuration.theme.sizes.map_height + configuration.theme.sizes.vertex_size) as i32; let offscreen = ImageSurface::create(Format::ARgb32, width, height)?; - let offscreen_ctx = Context::new(&offscreen)?; + let offscreen_context = Context::new(&offscreen)?; // Translate to account for vertex width/height - offscreen_ctx.translate( - config.theme.sizes.vertex_size / 2.0, - config.theme.sizes.vertex_size / 2.0, + offscreen_context.translate( + configuration.theme.sizes.vertex_size / 2.0, + configuration.theme.sizes.vertex_size / 2.0, ); for (group_index, group) in map.groups.iter().enumerate() { @@ -426,7 +471,8 @@ fn draw_groups( continue; } - let color = &config.theme.colors.groups[group_index % config.theme.colors.groups.len()]; + let color = &configuration.theme.colors.groups + [group_index % configuration.theme.colors.groups.len()]; let is_line = component_coords.len() == 2; let coords = if component_coords.len() == 1 { @@ -451,7 +497,7 @@ fn draw_groups( .collect(); let line_string: LineString = points.into(); - let hull = line_string.concave_hull(2.0); + let hull = line_string.concave_hull(); hull.exterior().coords().map(|c| (c.x, c.y)).collect() }; @@ -460,163 +506,205 @@ fn draw_groups( } // Draw path on offscreen context - set_color(&offscreen_ctx, color); - offscreen_ctx.move_to(coords[0].0, coords[0].1); + let (r, g, b) = *color; + offscreen_context.set_source_rgb(r, g, b); + offscreen_context.move_to(coords[0].0, coords[0].1); for &(x, y) in &coords[1..] { - offscreen_ctx.line_to(x, y); + offscreen_context.line_to(x, y); } // For polygons (not lines), close path and fill if !is_line { - offscreen_ctx.close_path(); - offscreen_ctx.fill_preserve()?; + offscreen_context.close_path(); + offscreen_context.fill_preserve()?; } // Draw stroke with round caps and joins for smoother appearance - offscreen_ctx.set_line_width(config.theme.sizes.vertex_size * 1.75); - offscreen_ctx.set_line_cap(LineCap::Round); - offscreen_ctx.set_line_join(LineJoin::Round); - offscreen_ctx.stroke()?; + offscreen_context.set_line_width(configuration.theme.sizes.vertex_size * 1.75); + offscreen_context.set_line_cap(LineCap::Round); + offscreen_context.set_line_join(LineJoin::Round); + offscreen_context.stroke()?; } // Composite entire offscreen surface onto main context with opacity (once for all groups) - ctx.save()?; - ctx.set_source_surface(&offscreen, 0.0, 0.0)?; - ctx.paint_with_alpha(config.theme.opacity.groups)?; - ctx.restore()?; + context.save()?; + context.set_source_surface(&offscreen, 0.0, 0.0)?; + context.paint_with_alpha(configuration.theme.opacity.groups)?; + context.restore()?; Ok(()) } fn draw_vertices( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, + configuration: &Configuration, ) -> Result<(), RenderError> { - use crate::shapes::draw_vertex; - - ctx.save()?; - set_color(ctx, &config.theme.colors.vertex); + context.save()?; + let (r, g, b) = configuration.theme.colors.vertex; + context.set_source_rgb(r, g, b); for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); + let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); + let y = percent_to_pixel( + component.coordinates.1, + configuration.theme.sizes.map_height, + ); draw_vertex( - ctx, + context, &component.shape, x, y, - config.theme.sizes.vertex_size, - config.theme.sizes.vertex_size, + configuration.theme.sizes.vertex_size, + configuration.theme.sizes.vertex_size, )?; } - ctx.restore()?; + context.restore()?; Ok(()) } fn draw_vertex_labels( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, - component_map: &std::collections::HashMap, + configuration: &Configuration, + component_map: &rustc_hash::FxHashMap, ) -> Result<(), RenderError> { - ctx.save()?; - set_color(ctx, &config.theme.colors.label); - ctx.select_font_face( - "sans-serif", + context.save()?; + let (r, g, b) = configuration.theme.colors.label; + context.set_source_rgb(r, g, b); + context.select_font_face( + &configuration.theme.fonts.face, FontSlant::Normal, FontWeight::Normal, ); - ctx.set_font_size(config.theme.fonts.vertex_label); - - if config.options.smart_label_positioning { - // Pre-compute collision data once for all labels - use crate::smart_positioning::{collect_all_lines, collect_note_rects, calculate_optimal_label_position_cached}; - - let all_lines = collect_all_lines( - map, - config.theme.sizes.map_width, - config.theme.sizes.map_height, - config.theme.sizes.vertex_size, - component_map, - ); + context.set_font_size(configuration.theme.fonts.vertex_label); - let note_rects = collect_note_rects( - map, - config.theme.sizes.map_width, - config.theme.sizes.map_height, - ctx, - config.theme.fonts.vertex_label, - )?; + if configuration.options.smart_label_positioning { + // For maps with very few dependencies, smart positioning overhead isn't worth it + let has_significant_complexity = map.dependencies.len() + map.evolutions.len() > 10; - for component in &map.components { - let position = calculate_optimal_label_position_cached( - component, - config.theme.sizes.map_width, - config.theme.sizes.map_height, - config.theme.sizes.vertex_size, - ctx, - config.theme.fonts.vertex_label, + if has_significant_complexity { + // Pre-compute collision data once for all labels + + let all_lines = collect_all_lines( + map, + configuration.theme.sizes.map_width, + configuration.theme.sizes.map_height, + configuration.theme.sizes.vertex_size, + component_map, + ); + + let spatial_grid = build_spatial_grid( &all_lines, - ¬e_rects, - )?; + configuration.theme.sizes.map_width, + configuration.theme.sizes.map_height, + ); - let label_x = position.x; - let label_y = position.y + 7.0; // Adjust Y position for text baseline + let note_rects = collect_note_rects( + map, + configuration.theme.sizes.map_width, + configuration.theme.sizes.map_height, + context, + configuration.theme.fonts.vertex_label, + )?; - ctx.move_to(label_x, label_y); - ctx.show_text(&component.label)?; + for component in &map.components { + let position = calculate_optimal_label_position_cached( + component, + &PositioningDimensions { + map_size: ( + configuration.theme.sizes.map_width, + configuration.theme.sizes.map_height, + ), + vertex_size: configuration.theme.sizes.vertex_size, + font_size: configuration.theme.fonts.vertex_label, + }, + context, + &all_lines, + &spatial_grid, + ¬e_rects, + )?; + + let label_x = position.x; + let label_y = position.y + 7.0; // Adjust Y position for text baseline + + context.move_to(label_x, label_y); + context.show_text(&component.label)?; + } + } else { + // Use simple positioning for simple maps + for component in &map.components { + let x = + percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); + let y = percent_to_pixel( + component.coordinates.1, + configuration.theme.sizes.map_height, + ); + let label_x = x + configuration.theme.sizes.vertex_size + 5.0; + let label_y = y + + configuration.theme.sizes.vertex_size / 2.0 + + configuration.theme.fonts.vertex_label / 3.0; + + context.move_to(label_x, label_y); + context.show_text(&component.label)?; + } } } else { // Simple label positioning (to the right of vertex) for component in &map.components { - let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height); - let label_x = x + config.theme.sizes.vertex_size + 5.0; - let label_y = y + config.theme.sizes.vertex_size / 2.0 + config.theme.fonts.vertex_label / 3.0; + let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width); + let y = percent_to_pixel( + component.coordinates.1, + configuration.theme.sizes.map_height, + ); + let label_x = x + configuration.theme.sizes.vertex_size + 5.0; + let label_y = y + + configuration.theme.sizes.vertex_size / 2.0 + + configuration.theme.fonts.vertex_label / 3.0; - ctx.move_to(label_x, label_y); - ctx.show_text(&component.label)?; + context.move_to(label_x, label_y); + context.show_text(&component.label)?; } } - ctx.restore()?; + context.restore()?; Ok(()) } fn draw_inertias( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, - component_map: &std::collections::HashMap, + configuration: &Configuration, + component_map: &rustc_hash::FxHashMap, ) -> Result<(), RenderError> { - ctx.save()?; - set_color(ctx, &config.theme.colors.inertia); + context.save()?; + let (r, g, b) = configuration.theme.colors.inertia; + context.set_source_rgb(r, g, b); let corner_radius = 2.0; - let rect_width = config.theme.sizes.vertex_size / 2.0; - let rect_height = config.theme.sizes.vertex_size * 2.0; + let rect_width = configuration.theme.sizes.vertex_size / 2.0; + let rect_height = configuration.theme.sizes.vertex_size * 2.0; for inertia in &map.inertias { if let Some(&(origin_x, origin_y)) = component_map.get(&inertia.component.to_lowercase()) { // Position relative to vertex top-left corner - let x = origin_x + 3.0 * config.theme.sizes.vertex_size; - let y = origin_y - (config.theme.sizes.vertex_size * 2.0) / 3.0; + let x = origin_x + 3.0 * configuration.theme.sizes.vertex_size; + let y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0; // Draw rounded rectangle - draw_rounded_rect(ctx, x, y, rect_width, rect_height, corner_radius)?; - ctx.fill()?; + draw_rounded_rect(context, x, y, rect_width, rect_height, corner_radius)?; + context.fill()?; } } - ctx.restore()?; + context.restore()?; Ok(()) } fn draw_rounded_rect( - ctx: &Context, + context: &Context, x: f64, y: f64, width: f64, @@ -624,45 +712,41 @@ fn draw_rounded_rect( radius: f64, ) -> Result<(), Error> { let r = radius; - ctx.new_path(); - ctx.arc( + context.new_path(); + context.arc( x + r, y + r, r, std::f64::consts::PI, 3.0 * std::f64::consts::PI / 2.0, ); - ctx.arc( + context.arc( x + width - r, y + r, r, 3.0 * std::f64::consts::PI / 2.0, 0.0, ); - ctx.arc( + context.arc( x + width - r, y + height - r, r, 0.0, std::f64::consts::PI / 2.0, ); - ctx.arc( + context.arc( x + r, y + height - r, r, std::f64::consts::PI / 2.0, std::f64::consts::PI, ); - ctx.close_path(); + context.close_path(); Ok(()) } /// Wraps text to fit within a given width, breaking at word boundaries -fn wrap_text( - ctx: &Context, - text: &str, - max_width: f64, -) -> Result, Error> { +fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result, Error> { let words: Vec<&str> = text.split_whitespace().collect(); let mut lines = Vec::new(); let mut current_line = String::new(); @@ -674,7 +758,7 @@ fn wrap_text( format!("{} {}", current_line, word) }; - let extents = ctx.text_extents(&test_line)?; + let extents = context.text_extents(&test_line)?; if extents.width() <= max_width { current_line = test_line; @@ -699,26 +783,26 @@ fn wrap_text( } fn draw_evolutions( - ctx: &Context, + context: &Context, map: &Map, - config: &Configuration, - component_map: &std::collections::HashMap, + configuration: &Configuration, + component_map: &rustc_hash::FxHashMap, ) -> Result<(), RenderError> { - use std::f64::consts::PI; - - ctx.save()?; - set_color(ctx, &config.theme.colors.evolution); - ctx.set_line_width(config.theme.sizes.line_width * 2.0); - ctx.set_dash(&[10.0], 0.0); + context.save()?; + let (r, g, b) = configuration.theme.colors.evolution; + context.set_source_rgb(r, g, b); + context.set_line_width(configuration.theme.sizes.line_width * 2.0); + context.set_dash(&[10.0], 0.0); for evolution in &map.evolutions { if let Some(&(origin_x, origin_y)) = component_map.get(&evolution.component.to_lowercase()) { // Calculate destination with clamping - let evolution_offset = percent_to_pixel(evolution.value, config.theme.sizes.map_width); + let evolution_offset = + percent_to_pixel(evolution.value, configuration.theme.sizes.map_width); let dest_x = (origin_x + evolution_offset) .max(0.0) - .min(config.theme.sizes.map_width); + .min(configuration.theme.sizes.map_width); let dest_y = origin_y; // Determine direction @@ -726,30 +810,30 @@ fn draw_evolutions( // Calculate offsets from top-left corner to center the line on the vertex let offset_origin_x = origin_x - + multiplier * (config.theme.sizes.vertex_size / 2.0) - + config.theme.sizes.vertex_size / 2.0; - let offset_origin_y = origin_y + config.theme.sizes.vertex_size / 2.0; - let offset_dest_x = dest_x - multiplier * (config.theme.sizes.vertex_size / 2.0) - + config.theme.sizes.vertex_size / 2.0; - let offset_dest_y = dest_y + config.theme.sizes.vertex_size / 2.0; + + multiplier * (configuration.theme.sizes.vertex_size / 2.0) + + configuration.theme.sizes.vertex_size / 2.0; + let offset_origin_y = origin_y + configuration.theme.sizes.vertex_size / 2.0; + let offset_dest_x = dest_x - multiplier * (configuration.theme.sizes.vertex_size / 2.0) + + configuration.theme.sizes.vertex_size / 2.0; + let offset_dest_y = dest_y + configuration.theme.sizes.vertex_size / 2.0; // Draw dashed line - ctx.move_to(offset_origin_x, offset_origin_y); - ctx.line_to(offset_dest_x, offset_dest_y); + context.move_to(offset_origin_x, offset_origin_y); + context.line_to(offset_dest_x, offset_dest_y); // Draw arrowhead at destination only let upper_angle = -PI / 4.0; let lower_angle = PI / 4.0; - let arrowhead_size = config.theme.sizes.arrowhead_size; + let arrowhead_size = configuration.theme.sizes.arrowhead_size; - ctx.move_to(offset_dest_x, offset_dest_y); - ctx.line_to( + context.move_to(offset_dest_x, offset_dest_y); + context.line_to( offset_dest_x - multiplier * arrowhead_size * upper_angle.cos(), offset_dest_y - multiplier * arrowhead_size * upper_angle.sin(), ); - ctx.move_to(offset_dest_x, offset_dest_y); - ctx.line_to( + context.move_to(offset_dest_x, offset_dest_y); + context.line_to( offset_dest_x - multiplier * arrowhead_size * lower_angle.cos(), offset_dest_y - multiplier * arrowhead_size * lower_angle.sin(), ); @@ -757,27 +841,31 @@ fn draw_evolutions( } // Batch stroke all paths at once - ctx.stroke()?; + context.stroke()?; - ctx.restore()?; + context.restore()?; Ok(()) } -fn draw_notes(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), RenderError> { +fn draw_notes( + context: &Context, + map: &Map, + configuration: &Configuration, +) -> Result<(), RenderError> { let padding = 5.0; let max_width = 400.0; - ctx.save()?; - ctx.select_font_face( - "sans-serif", + context.save()?; + context.select_font_face( + &configuration.theme.fonts.face, FontSlant::Normal, FontWeight::Normal, ); - ctx.set_font_size(config.theme.fonts.note); + context.set_font_size(configuration.theme.fonts.note); for note in &map.notes { - let x = percent_to_pixel(note.coordinates.0, config.theme.sizes.map_width); - let y = percent_to_pixel(note.coordinates.1, config.theme.sizes.map_height); + let x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width); + let y = percent_to_pixel(note.coordinates.1, configuration.theme.sizes.map_height); // Split text by newlines (handle both \\n escape and actual newlines) let text = note.text.replace("\\n", "\n"); @@ -786,45 +874,48 @@ fn draw_notes(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), Re // Calculate box dimensions let mut widest_line = 0.0; for line in &lines { - let extents = ctx.text_extents(line)?; + let extents = context.text_extents(line)?; if extents.width() > widest_line { widest_line = extents.width(); } } let box_width = f64::min(max_width, widest_line + padding * 2.0); - let box_height = lines.len() as f64 * config.theme.line_heights.note + padding * 2.0; + let box_height = lines.len() as f64 * configuration.theme.line_heights.note + padding * 2.0; // Draw background - set_color(ctx, &config.theme.colors.background); - ctx.rectangle(x, y, box_width, box_height); - ctx.fill()?; + let (r, g, b) = configuration.theme.colors.background; + context.set_source_rgb(r, g, b); + context.rectangle(x, y, box_width, box_height); + context.fill()?; // Draw border - set_color(ctx, &config.theme.colors.vertex); - ctx.set_line_width(config.theme.sizes.line_width); - ctx.rectangle(x, y, box_width, box_height); - ctx.stroke()?; + let (r, g, b) = configuration.theme.colors.vertex; + context.set_source_rgb(r, g, b); + context.set_line_width(configuration.theme.sizes.line_width); + context.rectangle(x, y, box_width, box_height); + context.stroke()?; // Draw text - set_color(ctx, &config.theme.colors.label); + let (r, g, b) = configuration.theme.colors.label; + context.set_source_rgb(r, g, b); for (i, line) in lines.iter().enumerate() { - ctx.move_to( + context.move_to( x + padding, - y + padding + i as f64 * config.theme.line_heights.note + config.theme.fonts.note, + y + padding + + i as f64 * configuration.theme.line_heights.note + + configuration.theme.fonts.note, ); - ctx.show_text(line)?; + context.show_text(line)?; } } - ctx.restore()?; + context.restore()?; Ok(()) } /// Gets the pixel positions of evolution stages -fn get_stage_positions(map: &Map, config: &Configuration) -> [f64; 3] { - use wmap_parser::Stage; - +fn get_stage_positions(map: &Map, configuration: &Configuration) -> [f64; 3] { let default_stages = [25.0, 50.0, 75.0]; let mut stages = default_stages; @@ -840,8 +931,8 @@ fn get_stage_positions(map: &Map, config: &Configuration) -> [f64; 3] { } [ - percent_to_pixel(stages[0], config.theme.sizes.map_width), - percent_to_pixel(stages[1], config.theme.sizes.map_width), - percent_to_pixel(stages[2], config.theme.sizes.map_width), + percent_to_pixel(stages[0], configuration.theme.sizes.map_width), + percent_to_pixel(stages[1], configuration.theme.sizes.map_width), + percent_to_pixel(stages[2], configuration.theme.sizes.map_width), ] } -- cgit