aboutsummaryrefslogtreecommitdiff
path: root/src/renderer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.rs')
-rw-r--r--src/renderer.rs165
1 files changed, 98 insertions, 67 deletions
diff --git a/src/renderer.rs b/src/renderer.rs
index a673bac..69c3f70 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -32,16 +32,26 @@ use crate::utils::percent_to_pixel;
/// # Returns
///
/// A vector of bytes containing the PNG image data
+///
+/// # Errors
+///
+/// Returns `RenderError` if the surface creation, rendering, or PNG encoding fails
pub fn render_to_png(
map: &Map,
stage_type: StageType,
configuration: &Configuration,
) -> Result<Vec<u8>, RenderError> {
- let total_width =
- (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32;
+ // Safe cast: map dimensions are typically in the range of hundreds to thousands of pixels
+ // Cairo's i32 surface dimensions can handle up to 2^31-1, so truncation is not a concern for typical maps
+ #[allow(clippy::cast_possible_truncation)]
+ let total_width = (configuration.theme.sizes.map_width
+ + configuration.theme.sizes.padding * 2.0)
+ .round() as i32;
+ #[allow(clippy::cast_possible_truncation)]
let total_height = (configuration.theme.sizes.map_height
+ configuration.theme.sizes.padding
- + configuration.theme.sizes.bottom_padding) as i32;
+ + configuration.theme.sizes.bottom_padding)
+ .round() as i32;
let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?;
let context = Context::new(&surface)?;
@@ -63,17 +73,27 @@ pub fn render_to_png(
///
/// # Returns
///
-/// A string containing the SVG data
+/// An `ImageSurface` containing the rendered map
+///
+/// # Errors
+///
+/// Returns `RenderError` if the surface creation or rendering fails
pub fn render_to_surface(
map: &Map,
stage_type: StageType,
configuration: &Configuration,
) -> Result<ImageSurface, RenderError> {
- let total_width =
- (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32;
+ // Safe cast: map dimensions are typically in the range of hundreds to thousands of pixels
+ // Cairo's i32 surface dimensions can handle up to 2^31-1, so truncation is not a concern for typical maps
+ #[allow(clippy::cast_possible_truncation)]
+ let total_width = (configuration.theme.sizes.map_width
+ + configuration.theme.sizes.padding * 2.0)
+ .round() as i32;
+ #[allow(clippy::cast_possible_truncation)]
let total_height = (configuration.theme.sizes.map_height
+ configuration.theme.sizes.padding
- + configuration.theme.sizes.bottom_padding) as i32;
+ + configuration.theme.sizes.bottom_padding)
+ .round() as i32;
let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?;
let context = Context::new(&surface)?;
@@ -94,6 +114,10 @@ pub fn render_to_surface(
/// # Returns
///
/// A string containing the SVG data
+///
+/// # Errors
+///
+/// Returns `RenderError` if the surface creation, rendering, or file I/O fails
pub fn render_to_svg(
map: &Map,
stage_type: StageType,
@@ -139,7 +163,7 @@ fn render(
// 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());
+ FxHashMap::with_capacity_and_hasher(map.components.len(), rustc_hash::FxBuildHasher);
for component in &map.components {
let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
let y = percent_to_pixel(
@@ -260,8 +284,8 @@ fn draw_axes(
let fonts = &configuration.theme.fonts;
context.save()?;
- let (r, g, b) = configuration.theme.colors.axis;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.axis;
+ context.set_source_rgb(red, green, blue);
context.set_line_width(sizes.line_width * 2.0);
// Y-axis and X-axis
@@ -271,8 +295,8 @@ fn draw_axes(
context.stroke()?;
// Text setup
- let (r, g, b) = configuration.theme.colors.label;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.label;
+ context.set_source_rgb(red, green, blue);
context.select_font_face(
&configuration.theme.fonts.face,
FontSlant::Normal,
@@ -323,7 +347,7 @@ fn draw_axes(
// Draw each stage label with wrapping
for (i, &label) in stage_labels.iter().enumerate() {
- let x = match i {
+ let stage_x = match i {
0 => 0.0,
1 => stages[0],
2 => stages[1],
@@ -334,8 +358,10 @@ fn draw_axes(
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;
- context.move_to(x, y);
+ // Safe cast: line_index is a small number (typically < 10 for wrapped stage labels)
+ #[allow(clippy::cast_precision_loss)]
+ let text_y = stage_label_y + line_index as f64 * line_height;
+ context.move_to(stage_x, text_y);
context.show_text(line)?;
}
}
@@ -383,13 +409,11 @@ fn draw_dependencies(
let arrow_angle = PI / 4.0; // 45 degrees
for dependency in &map.dependencies {
- let from_coords = match component_map.get(&dependency.from.to_lowercase()) {
- Some(coords) => coords,
- None => continue,
+ let Some(from_coords) = component_map.get(&dependency.from.to_lowercase()) else {
+ continue;
};
- let to_coords = match component_map.get(&dependency.to.to_lowercase()) {
- Some(coords) => coords,
- None => continue,
+ let Some(to_coords) = component_map.get(&dependency.to.to_lowercase()) else {
+ continue;
};
let origin_x = from_coords.0 + half_width;
@@ -447,8 +471,11 @@ fn draw_groups(
}
// Create ONE offscreen surface for ALL groups for opacity handling
+ // Safe cast: map dimensions plus vertex size are small values (< 10000 typically)
+ #[allow(clippy::cast_possible_truncation)]
let width =
(configuration.theme.sizes.map_width + configuration.theme.sizes.vertex_size) as i32;
+ #[allow(clippy::cast_possible_truncation)]
let height =
(configuration.theme.sizes.map_height + configuration.theme.sizes.vertex_size) as i32;
let offscreen = ImageSurface::create(Format::ARgb32, width, height)?;
@@ -482,7 +509,7 @@ fn draw_groups(
let segments = 16;
let mut circle_coords = Vec::new();
for i in 0..=segments {
- let angle = (i as f64 / segments as f64) * 2.0 * PI;
+ let angle = (f64::from(i) / f64::from(segments)) * 2.0 * PI;
circle_coords.push((x + radius * angle.cos(), y + radius * angle.sin()));
}
circle_coords
@@ -541,12 +568,13 @@ fn draw_vertices(
configuration: &Configuration,
) -> Result<(), RenderError> {
context.save()?;
- let (r, g, b) = configuration.theme.colors.vertex;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.vertex;
+ context.set_source_rgb(red, green, blue);
for component in &map.components {
- let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
- let y = percent_to_pixel(
+ let vertex_x =
+ percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
+ let vertex_y = percent_to_pixel(
component.coordinates.1,
configuration.theme.sizes.map_height,
);
@@ -554,8 +582,8 @@ fn draw_vertices(
draw_vertex(
context,
&component.shape,
- x,
- y,
+ vertex_x,
+ vertex_y,
configuration.theme.sizes.vertex_size,
configuration.theme.sizes.vertex_size,
)?;
@@ -572,8 +600,8 @@ fn draw_vertex_labels(
component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
context.save()?;
- let (r, g, b) = configuration.theme.colors.label;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.label;
+ context.set_source_rgb(red, green, blue);
context.select_font_face(
&configuration.theme.fonts.face,
FontSlant::Normal,
@@ -636,14 +664,14 @@ fn draw_vertex_labels(
} else {
// Use simple positioning for simple maps
for component in &map.components {
- let x =
+ let vertex_x =
percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
- let y = percent_to_pixel(
+ let vertex_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
+ let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0;
+ let label_y = vertex_y
+ configuration.theme.sizes.vertex_size / 2.0
+ configuration.theme.fonts.vertex_label / 3.0;
@@ -654,13 +682,14 @@ fn draw_vertex_labels(
} else {
// Simple label positioning (to the right of vertex)
for component in &map.components {
- let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
- let y = percent_to_pixel(
+ let vertex_x =
+ percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
+ let vertex_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
+ let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0;
+ let label_y = vertex_y
+ configuration.theme.sizes.vertex_size / 2.0
+ configuration.theme.fonts.vertex_label / 3.0;
@@ -680,8 +709,8 @@ fn draw_inertias(
component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
context.save()?;
- let (r, g, b) = configuration.theme.colors.inertia;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.inertia;
+ context.set_source_rgb(red, green, blue);
let corner_radius = 2.0;
let rect_width = configuration.theme.sizes.vertex_size / 2.0;
@@ -690,11 +719,18 @@ fn draw_inertias(
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 * configuration.theme.sizes.vertex_size;
- let y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0;
+ let inertia_x = origin_x + 3.0 * configuration.theme.sizes.vertex_size;
+ let inertia_y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0;
// Draw rounded rectangle
- draw_rounded_rect(context, x, y, rect_width, rect_height, corner_radius)?;
+ draw_rounded_rect(
+ context,
+ inertia_x,
+ inertia_y,
+ rect_width,
+ rect_height,
+ corner_radius,
+ );
context.fill()?;
}
}
@@ -703,14 +739,7 @@ fn draw_inertias(
Ok(())
}
-fn draw_rounded_rect(
- context: &Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
- radius: f64,
-) -> Result<(), Error> {
+fn draw_rounded_rect(context: &Context, x: f64, y: f64, width: f64, height: f64, radius: f64) {
let r = radius;
context.new_path();
context.arc(
@@ -742,7 +771,6 @@ fn draw_rounded_rect(
std::f64::consts::PI,
);
context.close_path();
- Ok(())
}
/// Wraps text to fit within a given width, breaking at word boundaries
@@ -755,7 +783,7 @@ fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result<Vec<String
let test_line = if current_line.is_empty() {
word.to_string()
} else {
- format!("{} {}", current_line, word)
+ format!("{current_line} {word}")
};
let extents = context.text_extents(&test_line)?;
@@ -864,8 +892,8 @@ fn draw_notes(
context.set_font_size(configuration.theme.fonts.note);
for note in &map.notes {
- 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);
+ let note_x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width);
+ let note_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");
@@ -881,30 +909,33 @@ fn draw_notes(
}
let box_width = f64::min(max_width, widest_line + padding * 2.0);
+ // Safe cast: number of lines in a note is small (typically < 100)
+ #[allow(clippy::cast_precision_loss)]
let box_height = lines.len() as f64 * configuration.theme.line_heights.note + padding * 2.0;
// Draw background
- let (r, g, b) = configuration.theme.colors.background;
- context.set_source_rgb(r, g, b);
- context.rectangle(x, y, box_width, box_height);
+ let (red, green, blue) = configuration.theme.colors.background;
+ context.set_source_rgb(red, green, blue);
+ context.rectangle(note_x, note_y, box_width, box_height);
context.fill()?;
// Draw border
- let (r, g, b) = configuration.theme.colors.vertex;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.vertex;
+ context.set_source_rgb(red, green, blue);
context.set_line_width(configuration.theme.sizes.line_width);
- context.rectangle(x, y, box_width, box_height);
+ context.rectangle(note_x, note_y, box_width, box_height);
context.stroke()?;
// Draw text
- let (r, g, b) = configuration.theme.colors.label;
- context.set_source_rgb(r, g, b);
+ let (red, green, blue) = configuration.theme.colors.label;
+ context.set_source_rgb(red, green, blue);
for (i, line) in lines.iter().enumerate() {
+ // Safe cast: line index is a small number (typically < 100 for note text)
+ #[allow(clippy::cast_precision_loss)]
+ let line_offset = i as f64 * configuration.theme.line_heights.note;
context.move_to(
- x + padding,
- y + padding
- + i as f64 * configuration.theme.line_heights.note
- + configuration.theme.fonts.note,
+ note_x + padding,
+ note_y + padding + line_offset + configuration.theme.fonts.note,
);
context.show_text(line)?;
}