aboutsummaryrefslogtreecommitdiff
path: root/src/renderer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.rs')
-rw-r--r--src/renderer.rs773
1 files changed, 432 insertions, 341 deletions
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};
+use crate::utils::percent_to_pixel;
-/// Renders a map to PNG format
+/// 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<Vec<u8>, 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<ImageSurface, 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)?;
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<String, RenderError> {
- 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);
+ context.save()?;
+ context.translate(
+ configuration.theme.sizes.padding,
+ configuration.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();
+ // 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<String, (f64, f64)> =
+ 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)?;
+ 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)?;
- ctx.restore()?;
+ 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, configuration);
- 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,
- )?,
- ];
+ // 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,
+ )?;
- ctx.save()?;
+ 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()?;
+ 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()?;
- 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, 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<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> 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<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> 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<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> 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);
+ context.set_font_size(configuration.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};
+ 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;
- let all_lines = collect_all_lines(
- map,
- config.theme.sizes.map_width,
- config.theme.sizes.map_height,
- config.theme.sizes.vertex_size,
- component_map,
- );
+ if has_significant_complexity {
+ // Pre-compute collision data once for all labels
- let note_rects = collect_note_rects(
- map,
- config.theme.sizes.map_width,
- config.theme.sizes.map_height,
- ctx,
- config.theme.fonts.vertex_label,
- )?;
+ let all_lines = collect_all_lines(
+ map,
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ configuration.theme.sizes.vertex_size,
+ component_map,
+ );
- 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,
+ let spatial_grid = build_spatial_grid(
&all_lines,
- &note_rects,
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ );
+
+ let note_rects = collect_note_rects(
+ map,
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ context,
+ configuration.theme.fonts.vertex_label,
)?;
- let label_x = position.x;
- let label_y = position.y + 7.0; // Adjust Y position for text baseline
+ 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,
+ &note_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;
- ctx.move_to(label_x, label_y);
- ctx.show_text(&component.label)?;
+ 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<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> 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<Vec<String>, Error> {
+fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result<Vec<String>, 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<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> 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),
]
}