aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/configuration.rs22
-rw-r--r--src/geometry.rs63
-rw-r--r--src/patterns.rs8
-rw-r--r--src/renderer.rs165
-rw-r--r--src/smart_positioning.rs4
-rw-r--r--src/stage_type.rs3
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 {