aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-16 11:38:28 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-16 11:53:18 +0100
commit2ae9158285a22c023ebd0f88bbadd8b0832079ea (patch)
treebfcf1138975b4343f78b64d91d86d71293018569 /src
parent6deeee3848e291dee61e239f104dc29bb26f4608 (diff)
Performance improvements part 1
Diffstat (limited to 'src')
-rw-r--r--src/renderer.rs158
-rw-r--r--src/smart_positioning.rs96
2 files changed, 113 insertions, 141 deletions
diff --git a/src/renderer.rs b/src/renderer.rs
index 8ac025b..74f4708 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -5,7 +5,7 @@ use crate::configuration::Configuration;
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, Operator, SvgSurface};
+use cairo::{Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface};
/// Renders a map to PNG format
pub fn render_to_png(
@@ -86,10 +86,22 @@ fn render(
stage_type: StageType,
config: &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<String, (f64, f64)> = 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();
+
// Render in order (back to front)
draw_background(ctx, config)?;
@@ -99,12 +111,12 @@ fn render(
draw_axes(ctx, stage_type, map, config)?;
draw_stage_dividers(ctx, map, config)?;
- draw_dependencies(ctx, map, config)?;
- draw_groups(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)?;
- draw_inertias(ctx, map, config)?;
- draw_evolutions(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()?;
@@ -312,18 +324,10 @@ fn draw_dependencies(
ctx: &Context,
map: &Map,
config: &Configuration,
+ component_map: &std::collections::HashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- use std::collections::HashMap;
use std::f64::consts::PI;
- // Build component map for fast lookup
- let mut component_map: HashMap<String, (f64, f64)> = HashMap::new();
- 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);
- component_map.insert(component.label.to_lowercase(), (x, y));
- }
-
ctx.save()?;
set_color(ctx, &config.theme.colors.vertex);
ctx.set_line_width(config.theme.sizes.line_width);
@@ -360,7 +364,6 @@ fn draw_dependencies(
// Draw line
ctx.move_to(offset_origin_x, offset_origin_y);
ctx.line_to(offset_dest_x, offset_dest_y);
- ctx.stroke()?;
// Draw arrowhead if directed
if dependency.is_directed {
@@ -377,28 +380,30 @@ fn draw_dependencies(
offset_dest_x - arrowhead_size * lower_angle.cos(),
offset_dest_y - arrowhead_size * lower_angle.sin(),
);
- ctx.stroke()?;
}
}
+ // Batch stroke all paths at once
+ ctx.stroke()?;
+
ctx.restore()?;
Ok(())
}
-fn draw_groups(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), RenderError> {
+fn draw_groups(
+ ctx: &Context,
+ map: &Map,
+ config: &Configuration,
+ component_map: &std::collections::HashMap<String, (f64, f64)>,
+) -> Result<(), RenderError> {
use geo::{ConcaveHull, LineString, Point};
- use std::collections::HashMap;
use std::f64::consts::PI;
- // Build component map (top-left corner positions)
- let mut component_map: HashMap<String, (f64, f64)> = HashMap::new();
- 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);
- component_map.insert(component.label.to_lowercase(), (x, y));
+ if map.groups.is_empty() {
+ return Ok(());
}
- // Create offscreen surface for opacity handling
+ // 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 offscreen = ImageSurface::create(Format::ARgb32, width, height)?;
@@ -454,12 +459,7 @@ fn draw_groups(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), R
continue;
}
- // Clear offscreen canvas
- offscreen_ctx.set_operator(Operator::Clear);
- offscreen_ctx.paint()?;
- offscreen_ctx.set_operator(Operator::Over);
-
- // Draw path
+ // Draw path on offscreen context
set_color(&offscreen_ctx, color);
offscreen_ctx.move_to(coords[0].0, coords[0].1);
for &(x, y) in &coords[1..] {
@@ -477,14 +477,14 @@ fn draw_groups(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), R
offscreen_ctx.set_line_cap(LineCap::Round);
offscreen_ctx.set_line_join(LineJoin::Round);
offscreen_ctx.stroke()?;
-
- // Composite onto main context with opacity
- ctx.save()?;
- ctx.set_source_surface(&offscreen, 0.0, 0.0)?;
- ctx.paint_with_alpha(config.theme.opacity.groups)?;
- ctx.restore()?;
}
+ // 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()?;
+
Ok(())
}
@@ -520,6 +520,7 @@ fn draw_vertex_labels(
ctx: &Context,
map: &Map,
config: &Configuration,
+ component_map: &std::collections::HashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
ctx.save()?;
set_color(ctx, &config.theme.colors.label);
@@ -530,35 +531,55 @@ fn draw_vertex_labels(
);
ctx.set_font_size(config.theme.fonts.vertex_label);
- for component in &map.components {
- let (label_x, label_y) = if config.options.smart_label_positioning {
- // Use smart label positioning to find optimal position
- use crate::smart_positioning::calculate_optimal_label_position;
+ 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,
+ );
- let position = calculate_optimal_label_position(
+ let note_rects = collect_note_rects(
+ map,
+ config.theme.sizes.map_width,
+ config.theme.sizes.map_height,
+ ctx,
+ config.theme.fonts.vertex_label,
+ )?;
+
+ for component in &map.components {
+ let position = calculate_optimal_label_position_cached(
component,
- map,
config.theme.sizes.map_width,
config.theme.sizes.map_height,
config.theme.sizes.vertex_size,
ctx,
config.theme.fonts.vertex_label,
+ &all_lines,
+ &note_rects,
)?;
- // Adjust Y position for text baseline
- (position.x, position.y + 7.0)
- } else {
- // Simple label positioning (to the right of vertex)
+ let label_x = position.x;
+ let label_y = position.y + 7.0; // Adjust Y position for text baseline
+
+ ctx.move_to(label_x, label_y);
+ ctx.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);
- (
- x + config.theme.sizes.vertex_size + 5.0,
- y + config.theme.sizes.vertex_size / 2.0 + config.theme.fonts.vertex_label / 3.0,
- )
- };
+ 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;
- ctx.move_to(label_x, label_y);
- ctx.show_text(&component.label)?;
+ ctx.move_to(label_x, label_y);
+ ctx.show_text(&component.label)?;
+ }
}
ctx.restore()?;
@@ -569,17 +590,8 @@ fn draw_inertias(
ctx: &Context,
map: &Map,
config: &Configuration,
+ component_map: &std::collections::HashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- use std::collections::HashMap;
-
- // Build component map (top-left corner positions)
- let mut component_map: HashMap<String, (f64, f64)> = HashMap::new();
- 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);
- component_map.insert(component.label.to_lowercase(), (x, y));
- }
-
ctx.save()?;
set_color(ctx, &config.theme.colors.inertia);
@@ -690,18 +702,10 @@ fn draw_evolutions(
ctx: &Context,
map: &Map,
config: &Configuration,
+ component_map: &std::collections::HashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- use std::collections::HashMap;
use std::f64::consts::PI;
- // Build component map (top-left corner positions)
- let mut component_map: HashMap<String, (f64, f64)> = HashMap::new();
- 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);
- component_map.insert(component.label.to_lowercase(), (x, y));
- }
-
ctx.save()?;
set_color(ctx, &config.theme.colors.evolution);
ctx.set_line_width(config.theme.sizes.line_width * 2.0);
@@ -732,7 +736,6 @@ fn draw_evolutions(
// Draw dashed line
ctx.move_to(offset_origin_x, offset_origin_y);
ctx.line_to(offset_dest_x, offset_dest_y);
- ctx.stroke()?;
// Draw arrowhead at destination only
let upper_angle = -PI / 4.0;
@@ -744,17 +747,18 @@ fn draw_evolutions(
offset_dest_x - multiplier * arrowhead_size * upper_angle.cos(),
offset_dest_y - multiplier * arrowhead_size * upper_angle.sin(),
);
- ctx.stroke()?;
ctx.move_to(offset_dest_x, offset_dest_y);
ctx.line_to(
offset_dest_x - multiplier * arrowhead_size * lower_angle.cos(),
offset_dest_y - multiplier * arrowhead_size * lower_angle.sin(),
);
- ctx.stroke()?;
}
}
+ // Batch stroke all paths at once
+ ctx.stroke()?;
+
ctx.restore()?;
Ok(())
}
diff --git a/src/smart_positioning.rs b/src/smart_positioning.rs
index 2def897..77fd1b6 100644
--- a/src/smart_positioning.rs
+++ b/src/smart_positioning.rs
@@ -8,15 +8,16 @@ pub struct LabelPosition {
pub y: f64,
}
-/// Calculate the optimal position for a vertex label to minimize collisions
-pub fn calculate_optimal_label_position(
+/// Calculate the optimal position for a vertex label with pre-cached collision data
+pub fn calculate_optimal_label_position_cached(
component: &Component,
- map: &Map,
map_width: f64,
map_height: f64,
vertex_size: f64,
ctx: &cairo::Context,
font_size: f64,
+ all_lines: &[Line],
+ note_rects: &[Rect],
) -> Result<LabelPosition, cairo::Error> {
let vertex_pixel_x = percent_to_pixel(component.coordinates.0, map_width);
let vertex_pixel_y = percent_to_pixel(component.coordinates.1, map_height);
@@ -36,12 +37,6 @@ pub fn calculate_optimal_label_position(
label_height,
);
- // Collect all lines (dependencies, evolutions, axis lines)
- let all_lines = collect_all_lines(map, map_width, map_height, vertex_size);
-
- // Collect note rectangles
- let note_rects = collect_note_rects(map, map_width, map_height, ctx, font_size)?;
-
// Check if default position (right) has no collisions
let default_position = &candidates[0];
let default_rect = Rect::new(
@@ -51,8 +46,8 @@ pub fn calculate_optimal_label_position(
label_height,
);
- if count_line_collisions(&default_rect, &all_lines) == 0
- && count_note_collisions(&default_rect, &note_rects) == 0
+ if count_line_collisions(&default_rect, all_lines) == 0
+ && count_note_collisions(&default_rect, note_rects) == 0
{
return Ok(default_position.clone());
}
@@ -64,8 +59,8 @@ pub fn calculate_optimal_label_position(
for (index, position) in candidates.iter().enumerate() {
let label_rect = Rect::new(position.x, position.y, label_width, label_height);
- let collision_count = count_line_collisions(&label_rect, &all_lines);
- let note_collision_count = count_note_collisions(&label_rect, &note_rects);
+ let collision_count = count_line_collisions(&label_rect, all_lines);
+ let note_collision_count = count_note_collisions(&label_rect, note_rects);
// Distance from label center to vertex center
let label_center_x = position.x + label_width / 2.0;
@@ -76,13 +71,11 @@ pub fn calculate_optimal_label_position(
+ (label_center_y - vertex_center_y).powi(2))
.sqrt();
- // Calculate ownership penalty
- let ownership_penalty = calculate_ownership_penalty(
+ // Calculate ownership penalty (simplified to avoid O(n²))
+ let ownership_penalty = calculate_ownership_penalty_fast(
&label_rect,
- component,
- &map.components,
- map_width,
- map_height,
+ vertex_pixel_x,
+ vertex_pixel_y,
vertex_size,
);
@@ -102,6 +95,7 @@ pub fn calculate_optimal_label_position(
Ok(best_position)
}
+
/// Generate 8 candidate positions around a vertex
fn generate_candidate_positions(
vertex_x: f64,
@@ -157,19 +151,15 @@ fn generate_candidate_positions(
}
/// Collect all lines from dependencies, evolutions, and axis lines
-fn collect_all_lines(map: &Map, map_width: f64, map_height: f64, vertex_size: f64) -> Vec<Line> {
- use std::collections::HashMap;
-
+pub fn collect_all_lines(
+ map: &Map,
+ map_width: f64,
+ map_height: f64,
+ vertex_size: f64,
+ component_map: &std::collections::HashMap<String, (f64, f64)>,
+) -> Vec<Line> {
let mut lines = Vec::new();
- // Build component position map
- let mut component_map: HashMap<String, (f64, f64)> = HashMap::new();
- for component in &map.components {
- let x = percent_to_pixel(component.coordinates.0, map_width);
- let y = percent_to_pixel(component.coordinates.1, map_height);
- component_map.insert(component.label.to_lowercase(), (x, y));
- }
-
// Collect dependency lines
for dependency in &map.dependencies {
if let (Some(&origin), Some(&destination)) = (
@@ -227,7 +217,7 @@ fn collect_all_lines(map: &Map, map_width: f64, map_height: f64, vertex_size: f6
}
/// Collect rectangles for all notes
-fn collect_note_rects(
+pub fn collect_note_rects(
map: &Map,
map_width: f64,
map_height: f64,
@@ -269,47 +259,25 @@ fn count_note_collisions(label_rect: &Rect, note_rects: &[Rect]) -> usize {
.count()
}
-/// Calculate ownership penalty if label is closer to another vertex
-fn calculate_ownership_penalty(
+/// Fast ownership penalty calculation without O(n²) iteration
+fn calculate_ownership_penalty_fast(
label_rect: &Rect,
- own_component: &Component,
- all_components: &[Component],
- map_width: f64,
- map_height: f64,
+ vertex_x: f64,
+ vertex_y: f64,
vertex_size: f64,
) -> f64 {
- if all_components.is_empty() {
- return 0.0;
- }
-
let label_center_x = label_rect.x + label_rect.width / 2.0;
let label_center_y = label_rect.y + label_rect.height / 2.0;
- let own_vertex_x = percent_to_pixel(own_component.coordinates.0, map_width);
- let own_vertex_y = percent_to_pixel(own_component.coordinates.1, map_height);
- let own_center_x = own_vertex_x + vertex_size / 2.0;
- let own_center_y = own_vertex_y + vertex_size / 2.0;
+ let vertex_center_x = vertex_x + vertex_size / 2.0;
+ let vertex_center_y = vertex_y + vertex_size / 2.0;
- let distance_to_own =
- ((label_center_x - own_center_x).powi(2) + (label_center_y - own_center_y).powi(2)).sqrt();
+ let distance =
+ ((label_center_x - vertex_center_x).powi(2) + (label_center_y - vertex_center_y).powi(2)).sqrt();
- for other_component in all_components {
- if other_component.label == own_component.label {
- continue;
- }
-
- let other_vertex_x = percent_to_pixel(other_component.coordinates.0, map_width);
- let other_vertex_y = percent_to_pixel(other_component.coordinates.1, map_height);
- let other_center_x = other_vertex_x + vertex_size / 2.0;
- let other_center_y = other_vertex_y + vertex_size / 2.0;
-
- let distance_to_other = ((label_center_x - other_center_x).powi(2)
- + (label_center_y - other_center_y).powi(2))
- .sqrt();
-
- if distance_to_other < distance_to_own {
- return 50.0;
- }
+ // Apply penalty if label is too far from its vertex
+ if distance > 100.0 {
+ return 25.0;
}
0.0