diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-16 16:35:08 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-16 17:38:30 +0100 |
| commit | bf4b51af43005415deac6ba475992c83b5f2f82e (patch) | |
| tree | 4bdc419138a070096735a45a74ea5eaf8822c691 /src | |
| parent | afd0794104228cd20e177f41dc68e9ae7ecaabda (diff) | |
Adapt to edition 2024
Diffstat (limited to 'src')
| -rw-r--r-- | src/configuration.rs | 22 | ||||
| -rw-r--r-- | src/geometry.rs | 63 | ||||
| -rw-r--r-- | src/patterns.rs | 8 | ||||
| -rw-r--r-- | src/renderer.rs | 165 | ||||
| -rw-r--r-- | src/smart_positioning.rs | 4 | ||||
| -rw-r--r-- | src/stage_type.rs | 3 |
6 files changed, 165 insertions, 100 deletions
diff --git a/src/configuration.rs b/src/configuration.rs index bc99781..715e4a7 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -32,19 +32,19 @@ impl Default for Colors { fn default() -> Self { Self { background: (1.0, 1.0, 1.0), - axis: (0.058824, 0.149020, 0.121569), // Deep Slate Green - vertex: (0.058824, 0.149020, 0.121569), - label: (0.058824, 0.149020, 0.121569), - stage_foreground: (0.854901, 0.901960, 0.890196), + axis: (0.058_824, 0.149_020, 0.121_569), // Deep Slate Green + vertex: (0.058_824, 0.149_020, 0.121_569), + label: (0.058_824, 0.149_020, 0.121_569), + stage_foreground: (0.854_901, 0.901_960, 0.890_196), stage_background: (1.0, 1.0, 1.0), - inertia: (0.980392, 0.168627, 0.0), - evolution: (0.309804, 0.560784, 0.901961), + inertia: (0.980_392, 0.168_627, 0.0), + evolution: (0.309_804, 0.560_784, 0.901_961), groups: vec![ - (0.227451, 0.356863, 0.627451), // Olympic Blue - (0.768627, 0.270588, 0.211765), // Jasper Red - (0.560784, 0.745098, 0.572549), // Light Porcelain Green - (0.980392, 0.835294, 0.360784), // Naples Yellow - (0.925490, 0.611765, 0.670588), // Hermosa Pink + (0.227_451, 0.356_863, 0.627_451), // Olympic Blue + (0.768_627, 0.270_588, 0.211_765), // Jasper Red + (0.560_784, 0.745_098, 0.572_549), // Light Porcelain Green + (0.980_392, 0.835_294, 0.360_784), // Naples Yellow + (0.925_490, 0.611_765, 0.670_588), // Hermosa Pink ], } } diff --git a/src/geometry.rs b/src/geometry.rs index 0841183..358620f 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -103,8 +103,12 @@ pub struct SpatialGrid { impl SpatialGrid { /// Creates a new spatial grid for the given dimensions pub fn new(width: f64, height: f64, cell_size: f64) -> Self { - let columns = ((width / cell_size).ceil() as usize).max(1); - let rows = ((height / cell_size).ceil() as usize).max(1); + // Safe cast: width and height are positive map dimensions, divided by cell_size and clamped to at least 1.0 + // The resulting grid size will be reasonable (< millions of cells) for typical map sizes + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let columns = (width / cell_size).ceil().max(1.0) as usize; + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let rows = (height / cell_size).ceil().max(1.0) as usize; let cells = vec![Vec::new(); columns * rows]; Self { @@ -122,10 +126,17 @@ impl SpatialGrid { let min_y = line.start.1.min(line.end.1); let max_y = line.start.1.max(line.end.1); - let start_column = ((min_x / self.cell_size).floor() as usize).min(self.columns - 1); - let end_column = ((max_x / self.cell_size).floor() as usize).min(self.columns - 1); - let start_row = ((min_y / self.cell_size).floor() as usize).min(self.rows - 1); - let end_row = ((max_y / self.cell_size).floor() as usize).min(self.rows - 1); + // Safe cast: coordinates are clamped to [0.0, max] before casting, ensuring valid grid indices + // The .max(0.0) ensures non-negative, and .min(columns/rows - 1) ensures within bounds + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let start_column = + ((min_x / self.cell_size).floor().max(0.0) as usize).min(self.columns - 1); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let end_column = ((max_x / self.cell_size).floor().max(0.0) as usize).min(self.columns - 1); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let start_row = ((min_y / self.cell_size).floor().max(0.0) as usize).min(self.rows - 1); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let end_row = ((max_y / self.cell_size).floor().max(0.0) as usize).min(self.rows - 1); for row in start_row..=end_row { for col in start_column..=end_column { @@ -143,18 +154,34 @@ impl SpatialGrid { let min_y = rect.y; let max_y = rect.y + rect.height; - let start_column = ((min_x / self.cell_size).floor() as isize) - .max(0) - .min(self.columns as isize - 1) as usize; - let end_column = ((max_x / self.cell_size).floor() as isize) - .max(0) - .min(self.columns as isize - 1) as usize; - let start_row = ((min_y / self.cell_size).floor() as isize) - .max(0) - .min(self.rows as isize - 1) as usize; - let end_row = ((max_y / self.cell_size).floor() as isize) - .max(0) - .min(self.rows as isize - 1) as usize; + let max_column_idx = if self.columns > 0 { + self.columns - 1 + } else { + 0 + }; + let max_row_idx = if self.rows > 0 { self.rows - 1 } else { 0 }; + + // Safe cast: grid indices are small (< thousands) for typical map sizes, well within f64 precision + // The coordinates are clamped to valid grid bounds before casting to usize + #[allow(clippy::cast_precision_loss)] + let max_column_f64 = max_column_idx as f64; + #[allow(clippy::cast_precision_loss)] + let max_row_f64 = max_row_idx as f64; + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let start_column = ((min_x / self.cell_size) + .floor() + .max(0.0) + .min(max_column_f64)) as usize; + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let end_column = ((max_x / self.cell_size) + .floor() + .max(0.0) + .min(max_column_f64)) as usize; + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let start_row = ((min_y / self.cell_size).floor().max(0.0).min(max_row_f64)) as usize; + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let end_row = ((max_y / self.cell_size).floor().max(0.0).min(max_row_f64)) as usize; let mut line_indices = Vec::new(); for row in start_row..=end_row { diff --git a/src/patterns.rs b/src/patterns.rs index 254433f..8fb2918 100644 --- a/src/patterns.rs +++ b/src/patterns.rs @@ -31,13 +31,13 @@ pub fn create_pattern( { let context = Context::new(&surface)?; - for y in 0..8 { - for x in 0..8 { - let value = pattern_data[y * 8 + x]; + for y in 0_u32..8 { + for x in 0_u32..8 { + let value = pattern_data[(y * 8 + x) as usize]; let (r, g, b) = if value == 0 { foreground } else { background }; context.set_source_rgb(r, g, b); - context.rectangle(x as f64, y as f64, 1.0, 1.0); + context.rectangle(f64::from(x), f64::from(y), 1.0, 1.0); context.fill()?; } } 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)?; } diff --git a/src/smart_positioning.rs b/src/smart_positioning.rs index 81a2c3d..55d3bf3 100644 --- a/src/smart_positioning.rs +++ b/src/smart_positioning.rs @@ -10,6 +10,8 @@ pub struct LabelPosition { pub y: f64, } +// All fields relate to sizing/dimensions, so the consistent postfix is intentional and clear +#[allow(clippy::struct_field_names)] #[derive(Debug, Clone)] pub struct PositioningDimensions { pub map_size: (f64, f64), @@ -89,6 +91,8 @@ pub fn calculate_optimal_label_position_cached( ); // Score: collisions * 100 + note_collisions * 500 + distance * 0.5 + index * 0.1 + ownership + // Safe casts: collision counts and position indices are small numbers (< 1000 typically) + #[allow(clippy::cast_precision_loss)] let score = (collision_count as f64) * 100.0 + (note_collision_count as f64) * 500.0 + distance * 0.5 diff --git a/src/stage_type.rs b/src/stage_type.rs index a003317..ae80166 100644 --- a/src/stage_type.rs +++ b/src/stage_type.rs @@ -57,6 +57,9 @@ impl StageType { } /// Returns the four evolution stage labels for this stage type + // This function is long because it contains comprehensive stage label definitions for multiple types + // Splitting it would reduce clarity and require additional complexity + #[allow(clippy::too_many_lines)] #[must_use] pub fn stages(&self) -> [&'static str; 4] { match self { |