aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/configuration.rs152
-rw-r--r--src/error.rs31
-rw-r--r--src/geometry.rs102
-rw-r--r--src/lib.rs102
-rw-r--r--src/patterns.rs37
-rw-r--r--src/renderer.rs773
-rw-r--r--src/shapes.rs97
-rw-r--r--src/smart_positioning.rs85
-rw-r--r--src/stage_type.rs2
-rw-r--r--src/utils.rs35
10 files changed, 767 insertions, 649 deletions
diff --git a/src/configuration.rs b/src/configuration.rs
index 775d6cd..bc99781 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -17,34 +17,34 @@ impl Default for Options {
/// Color configuration
#[derive(Debug, Clone)]
pub struct Colors {
- pub background: String,
- pub axis: String,
- pub vertex: String,
- pub label: String,
- pub stage_foreground: String,
- pub stage_background: String,
- pub inertia: String,
- pub evolution: String,
- pub groups: Vec<String>,
+ pub background: (f64, f64, f64),
+ pub axis: (f64, f64, f64),
+ pub vertex: (f64, f64, f64),
+ pub label: (f64, f64, f64),
+ pub stage_foreground: (f64, f64, f64),
+ pub stage_background: (f64, f64, f64),
+ pub inertia: (f64, f64, f64),
+ pub evolution: (f64, f64, f64),
+ pub groups: Vec<(f64, f64, f64)>,
}
impl Default for Colors {
fn default() -> Self {
Self {
- background: "#FFFFFF".to_string(),
- axis: "#0F261F".to_string(),
- vertex: "#0F261F".to_string(),
- label: "#0F261F".to_string(),
- stage_foreground: "#DAE6E3".to_string(),
- stage_background: "#FFFFFF".to_string(),
- inertia: "#FA2B00".to_string(),
- evolution: "#4F8FE6".to_string(),
+ 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),
+ stage_background: (1.0, 1.0, 1.0),
+ inertia: (0.980392, 0.168627, 0.0),
+ evolution: (0.309804, 0.560784, 0.901961),
groups: vec![
- "#3A5BA0".to_string(), // Olympic Blue
- "#C44536".to_string(), // Jasper Red
- "#8FBE92".to_string(), // Light Porcelain Green
- "#FAD55C".to_string(), // Naples Yellow
- "#EC9CAB".to_string(), // Hermosa Pink
+ (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
],
}
}
@@ -81,7 +81,7 @@ impl Default for Sizes {
/// Font configuration
#[derive(Debug, Clone)]
pub struct Fonts {
- pub family: String,
+ pub face: String,
pub axis_label: f64,
pub vertex_label: f64,
pub note: f64,
@@ -90,7 +90,7 @@ pub struct Fonts {
impl Default for Fonts {
fn default() -> Self {
Self {
- family: "Helvetica Neue, Helvetica, Arial, sans-serif".to_string(),
+ face: "sans-serif".to_string(),
axis_label: 14.0,
vertex_label: 12.0,
note: 12.0,
@@ -149,47 +149,89 @@ mod tests {
#[test]
fn test_default_configuration() {
- let config = Configuration::default();
+ let configuration = Configuration::default();
+ const ALLOWED_ERROR_VEHICLE_LENGTH_CM: f64 = 0.1;
// Test options
- assert!(config.options.show_background);
- assert!(config.options.smart_label_positioning);
+ assert!(configuration.options.show_background);
+ assert!(configuration.options.smart_label_positioning);
- // Test colors (matching JS implementation exactly)
- assert_eq!(config.theme.colors.background, "#FFFFFF");
- assert_eq!(config.theme.colors.axis, "#0F261F");
- assert_eq!(config.theme.colors.vertex, "#0F261F");
- assert_eq!(config.theme.colors.label, "#0F261F");
- assert_eq!(config.theme.colors.stage_foreground, "#DAE6E3");
- assert_eq!(config.theme.colors.stage_background, "#FFFFFF");
- assert_eq!(config.theme.colors.inertia, "#FA2B00");
- assert_eq!(config.theme.colors.evolution, "#4F8FE6");
- assert_eq!(config.theme.colors.groups.len(), 5);
+ // Test colors
+ assert_eq!(configuration.theme.colors.background, (1.0, 1.0, 1.0));
+ assert_eq!(
+ configuration.theme.colors.axis,
+ (0.058_824, 0.149_020, 0.121_569)
+ );
+ assert_eq!(
+ configuration.theme.colors.vertex,
+ (0.058_824, 0.149_020, 0.121_569)
+ );
+ assert_eq!(
+ configuration.theme.colors.label,
+ (0.058_824, 0.149_020, 0.121_569)
+ );
+ assert_eq!(
+ configuration.theme.colors.stage_foreground,
+ (0.854_901, 0.901_960, 0.890_196)
+ );
+ assert_eq!(configuration.theme.colors.stage_background, (1.0, 1.0, 1.0));
+ assert_eq!(
+ configuration.theme.colors.inertia,
+ (0.980_392, 0.168_627, 0.0)
+ );
+ assert_eq!(
+ configuration.theme.colors.evolution,
+ (0.309_804, 0.560_784, 0.901_961)
+ );
+ assert_eq!(configuration.theme.colors.groups.len(), 5);
- // Test sizes (matching JS implementation with extra bottom padding for wrapped labels)
- assert_eq!(config.theme.sizes.map_width, 1300.0);
- assert_eq!(config.theme.sizes.map_height, 1000.0);
- assert_eq!(config.theme.sizes.padding, 42.0);
- assert_eq!(config.theme.sizes.bottom_padding, 120.0);
- assert_eq!(config.theme.sizes.line_width, 0.5);
- assert_eq!(config.theme.sizes.vertex_size, 25.0);
- assert_eq!(config.theme.sizes.arrowhead_size, 10.0);
- assert_eq!(config.theme.sizes.stage_height, 100.0);
+ // Test sizes
+ assert!(
+ (configuration.theme.sizes.map_width - 1300.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.map_height - 1000.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!((configuration.theme.sizes.padding - 42.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM);
+ assert!(
+ (configuration.theme.sizes.bottom_padding - 120.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.line_width - 0.5).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.vertex_size - 25.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.arrowhead_size - 10.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.stage_height - 100.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
// Test fonts
- assert_eq!(
- config.theme.fonts.family,
- "Helvetica Neue, Helvetica, Arial, sans-serif"
+ assert_eq!(configuration.theme.fonts.face, "sans-serif");
+ assert!(
+ (configuration.theme.fonts.axis_label - 14.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.fonts.vertex_label - 12.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
);
- assert_eq!(config.theme.fonts.axis_label, 14.0);
- assert_eq!(config.theme.fonts.vertex_label, 12.0);
- assert_eq!(config.theme.fonts.note, 12.0);
+ assert!((configuration.theme.fonts.note - 12.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM);
// Test line heights
- assert_eq!(config.theme.line_heights.vertex_label, 18.0);
- assert_eq!(config.theme.line_heights.note, 18.0);
+ assert!(
+ (configuration.theme.line_heights.vertex_label - 18.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.line_heights.note - 18.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
// Test opacity
- assert_eq!(config.theme.opacity.groups, 0.1);
+ assert!((configuration.theme.opacity.groups - 0.1).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM);
}
}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..1d6790b
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,31 @@
+use cairo::{Error as CairoError, IoError};
+use std::io::Error as StdError;
+use thiserror::Error;
+
+/// Error types for rendering
+#[derive(Debug, Error)]
+pub enum RenderError {
+ #[error("Cairo rendering error: {0}")]
+ Cairo(String),
+
+ #[error("Invalid configuration: {0}")]
+ InvalidConfig(String),
+
+ #[error("Concave hull calculation failed")]
+ GeometryError,
+
+ #[error("IO error: {0}")]
+ Io(#[from] StdError),
+}
+
+impl From<CairoError> for RenderError {
+ fn from(err: CairoError) -> Self {
+ RenderError::Cairo(err.to_string())
+ }
+}
+
+impl From<IoError> for RenderError {
+ fn from(err: IoError) -> Self {
+ RenderError::Cairo(err.to_string())
+ }
+}
diff --git a/src/geometry.rs b/src/geometry.rs
index 5099ada..0841183 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -5,6 +5,12 @@ pub struct Line {
pub end: (f64, f64),
}
+impl Line {
+ pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
+ Self { start, end }
+ }
+}
+
/// Represents a rectangle
#[derive(Debug, Clone, Copy)]
pub struct Rect {
@@ -14,12 +20,6 @@ pub struct Rect {
pub height: f64,
}
-impl Line {
- pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
- Self { start, end }
- }
-}
-
impl Rect {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
@@ -71,28 +71,106 @@ pub fn line_intersects_line(l1: &Line, l2: &Line) -> bool {
/// Checks if a line segment intersects a rectangle
pub fn line_intersects_rect(line: &Line, rect: &Rect) -> bool {
- // Check if either endpoint is inside the rectangle
if point_in_rect(line.start, rect) || point_in_rect(line.end, rect) {
return true;
}
- // Check intersection with all four edges
let edges = [
- Line::new((rect.x, rect.y), (rect.x + rect.width, rect.y)), // Top
+ Line::new((rect.x, rect.y), (rect.x + rect.width, rect.y)),
Line::new(
(rect.x + rect.width, rect.y),
(rect.x + rect.width, rect.y + rect.height),
- ), // Right
+ ),
Line::new(
(rect.x + rect.width, rect.y + rect.height),
(rect.x, rect.y + rect.height),
- ), // Bottom
- Line::new((rect.x, rect.y + rect.height), (rect.x, rect.y)), // Left
+ ),
+ Line::new((rect.x, rect.y + rect.height), (rect.x, rect.y)),
];
edges.iter().any(|edge| line_intersects_line(line, edge))
}
+/// Spatial hash grid for fast collision detection
+/// that stores cells as indices of lines.
+pub struct SpatialGrid {
+ cell_size: f64,
+ cells: Vec<Vec<usize>>,
+ columns: usize,
+ rows: usize,
+}
+
+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);
+ let cells = vec![Vec::new(); columns * rows];
+
+ Self {
+ cell_size,
+ cells,
+ columns,
+ rows,
+ }
+ }
+
+ /// Inserts a line into the spatial grid
+ pub fn insert_line(&mut self, line_idx: usize, line: &Line) {
+ let min_x = line.start.0.min(line.end.0);
+ let max_x = line.start.0.max(line.end.0);
+ 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);
+
+ for row in start_row..=end_row {
+ for col in start_column..=end_column {
+ let cell_idx = row * self.columns + col;
+ self.cells[cell_idx].push(line_idx);
+ }
+ }
+ }
+
+ /// Queries the spatial grid for lines that might intersect with the given rectangle
+ /// Returns line indices without duplicates
+ pub fn query_rect(&self, rect: &Rect) -> impl Iterator<Item = usize> + '_ {
+ let min_x = rect.x;
+ let max_x = rect.x + rect.width;
+ 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 mut line_indices = Vec::new();
+ for row in start_row..=end_row {
+ for column in start_column..=end_column {
+ let cell_idx = row * self.columns + column;
+ line_indices.extend_from_slice(&self.cells[cell_idx]);
+ }
+ }
+
+ line_indices.sort_unstable();
+ line_indices.dedup();
+
+ line_indices.into_iter()
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/lib.rs b/src/lib.rs
index de4146d..52d10aa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
mod configuration;
+mod error;
mod geometry;
mod patterns;
mod renderer;
@@ -7,114 +8,21 @@ mod smart_positioning;
mod stage_type;
mod utils;
-// Re-export parser types
-pub use wmap_parser::{
- Component, Dependency, Evolution, Group, Inertia, Map, Note, Shape, Stage, StageData,
-};
-
-// Re-export our types
pub use configuration::{
Colors, Configuration, Fonts, LineHeights, Opacity, Options, Sizes, Theme,
};
+pub use renderer::{render_to_png, render_to_surface, render_to_svg};
pub use stage_type::StageType;
-use thiserror::Error;
-use cairo::ImageSurface;
-
-/// Error types for rendering
-#[derive(Debug, Error)]
-pub enum RenderError {
- #[error("Cairo rendering error: {0}")]
- Cairo(String),
-
- #[error("Invalid configuration: {0}")]
- InvalidConfig(String),
-
- #[error("Concave hull calculation failed")]
- GeometryError,
-
- #[error("IO error: {0}")]
- Io(#[from] std::io::Error),
-}
-
-impl From<cairo::Error> for RenderError {
- fn from(err: cairo::Error) -> Self {
- RenderError::Cairo(err.to_string())
- }
-}
-
-impl From<cairo::IoError> for RenderError {
- fn from(err: cairo::IoError) -> Self {
- RenderError::Cairo(err.to_string())
- }
-}
-
-/// 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,
-) -> Result<Vec<u8>, RenderError> {
- renderer::render_to_png(map, stage_type, config)
-}
-
-/// 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,
-) -> Result<String, RenderError> {
- renderer::render_to_svg(map, stage_type, config)
-}
-
-/// 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,
-) -> Result<ImageSurface, RenderError> {
- renderer::render_to_surface(map, stage_type, config)
-}
-
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_configuration() {
- let config = Configuration::default();
- assert!(config.options.show_background);
- assert!(config.options.smart_label_positioning);
+ let configuration = Configuration::default();
+ assert!(configuration.options.show_background);
+ assert!(configuration.options.smart_label_positioning);
}
#[test]
diff --git a/src/patterns.rs b/src/patterns.rs
index 12c790a..254433f 100644
--- a/src/patterns.rs
+++ b/src/patterns.rs
@@ -1,7 +1,5 @@
-use crate::utils::parse_color;
+use cairo::{Context, Error, Extend, Format, ImageSurface, SurfacePattern};
-/// 8x8 1-bit pattern data
-/// 0 = foreground color, 1 = background color
pub const STITCH: [u8; 64] = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
@@ -25,30 +23,28 @@ pub const WICKER: [u8; 64] = [
/// Creates a cairo surface pattern from pattern data
pub fn create_pattern(
pattern_data: &[u8; 64],
- foreground: &str,
- background: &str,
-) -> Result<cairo::SurfacePattern, cairo::Error> {
- let surface = cairo::ImageSurface::create(cairo::Format::Rgb24, 8, 8)?;
+ foreground: (f64, f64, f64),
+ background: (f64, f64, f64),
+) -> Result<SurfacePattern, Error> {
+ let surface = ImageSurface::create(Format::Rgb24, 8, 8)?;
{
- let ctx = cairo::Context::new(&surface)?;
- let fg = parse_color(foreground);
- let bg = parse_color(background);
+ let context = Context::new(&surface)?;
for y in 0..8 {
for x in 0..8 {
let value = pattern_data[y * 8 + x];
- let (r, g, b) = if value == 0 { fg } else { bg };
+ let (r, g, b) = if value == 0 { foreground } else { background };
- ctx.set_source_rgb(r, g, b);
- ctx.rectangle(x as f64, y as f64, 1.0, 1.0);
- ctx.fill()?;
+ context.set_source_rgb(r, g, b);
+ context.rectangle(x as f64, y as f64, 1.0, 1.0);
+ context.fill()?;
}
}
}
- let pattern = cairo::SurfacePattern::create(&surface);
- pattern.set_extend(cairo::Extend::Repeat);
+ let pattern = SurfacePattern::create(&surface);
+ pattern.set_extend(Extend::Repeat);
Ok(pattern)
}
@@ -66,12 +62,11 @@ mod tests {
#[test]
fn test_pattern_values() {
- // All pattern values should be 0 or 1
- for &val in &STITCH {
- assert!(val == 0 || val == 1);
+ for &pixel in &STITCH {
+ assert!(pixel == 0 || pixel == 1);
}
- for &val in &SHINGLES {
- assert!(val == 0 || val == 1);
+ for &pixel in &SHINGLES {
+ assert!(pixel == 0 || pixel == 1);
}
}
}
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),
]
}
diff --git a/src/shapes.rs b/src/shapes.rs
index 2b8db2c..bf408be 100644
--- a/src/shapes.rs
+++ b/src/shapes.rs
@@ -1,95 +1,74 @@
+use cairo::{Context, Error};
+use std::f64::consts::PI;
use wmap_parser::Shape;
/// Draws a vertex shape at the given position
pub fn draw_vertex(
- ctx: &cairo::Context,
+ context: &Context,
shape: &Shape,
x: f64,
y: f64,
width: f64,
height: f64,
-) -> Result<(), cairo::Error> {
+) -> Result<(), Error> {
match shape {
- Shape::Circle => draw_circle(ctx, x, y, width, height),
- Shape::Square => draw_square(ctx, x, y, width, height),
- Shape::Triangle => draw_triangle(ctx, x, y, width, height),
- Shape::X => draw_x(ctx, x, y, width, height),
+ Shape::Circle => draw_circle(context, x, y, width, height),
+ Shape::Square => draw_square(context, x, y, width, height),
+ Shape::Triangle => draw_triangle(context, x, y, width, height),
+ Shape::X => draw_x(context, x, y, width, height),
}
}
-fn draw_circle(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
- ctx.save()?;
- ctx.translate(x + width / 2.0, y + height / 2.0);
- ctx.scale(width / 2.0, height / 2.0);
- ctx.arc(0.0, 0.0, 1.0, 0.0, 2.0 * std::f64::consts::PI);
- ctx.restore()?;
- ctx.fill()?;
+fn draw_circle(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
+ context.save()?;
+ context.translate(x + width / 2.0, y + height / 2.0);
+ context.scale(width / 2.0, height / 2.0);
+ context.arc(0.0, 0.0, 1.0, 0.0, 2.0 * PI);
+ context.restore()?;
+ context.fill()?;
Ok(())
}
-fn draw_square(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
- ctx.rectangle(x, y, width, height);
- ctx.fill()?;
+fn draw_square(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
+ context.rectangle(x, y, width, height);
+ context.fill()?;
Ok(())
}
-fn draw_triangle(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
- ctx.move_to(x + width / 2.0, y);
- ctx.line_to(x + width, y + height);
- ctx.line_to(x, y + height);
- ctx.close_path();
- ctx.fill()?;
+fn draw_triangle(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
+ context.move_to(x + width / 2.0, y);
+ context.line_to(x + width, y + height);
+ context.line_to(x, y + height);
+ context.close_path();
+ context.fill()?;
Ok(())
}
-fn draw_x(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
+fn draw_x(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
let line_width = 2.0;
- ctx.set_line_width(line_width);
- ctx.move_to(x, y);
- ctx.line_to(x + width, y + height);
- ctx.move_to(x + width, y);
- ctx.line_to(x, y + height);
- ctx.stroke()?;
+ context.set_line_width(line_width);
+ context.move_to(x, y);
+ context.line_to(x + width, y + height);
+ context.move_to(x + width, y);
+ context.line_to(x, y + height);
+ context.stroke()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
+ use cairo::{Context, Format, ImageSurface};
#[test]
fn test_draw_shapes() {
// Just verify the functions don't panic
- let surface = cairo::ImageSurface::create(cairo::Format::Rgb24, 100, 100).unwrap();
- let ctx = cairo::Context::new(&surface).unwrap();
+ let surface = ImageSurface::create(Format::Rgb24, 100, 100).unwrap();
+ let context = Context::new(&surface).unwrap();
- assert!(draw_vertex(&ctx, &Shape::Circle, 0.0, 0.0, 25.0, 25.0).is_ok());
- assert!(draw_vertex(&ctx, &Shape::Square, 0.0, 0.0, 25.0, 25.0).is_ok());
- assert!(draw_vertex(&ctx, &Shape::Triangle, 0.0, 0.0, 25.0, 25.0).is_ok());
- assert!(draw_vertex(&ctx, &Shape::X, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::Circle, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::Square, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::Triangle, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::X, 0.0, 0.0, 25.0, 25.0).is_ok());
}
}
diff --git a/src/smart_positioning.rs b/src/smart_positioning.rs
index 77fd1b6..81a2c3d 100644
--- a/src/smart_positioning.rs
+++ b/src/smart_positioning.rs
@@ -1,5 +1,7 @@
-use crate::geometry::{Line, Rect};
+use crate::geometry::{Line, Rect, SpatialGrid};
use crate::utils::percent_to_pixel;
+
+use cairo::{Context, Error};
use wmap_parser::{Component, Map};
#[derive(Debug, Clone)]
@@ -8,23 +10,30 @@ pub struct LabelPosition {
pub y: f64,
}
-/// Calculate the optimal position for a vertex label with pre-cached collision data
+#[derive(Debug, Clone)]
+pub struct PositioningDimensions {
+ pub map_size: (f64, f64),
+ pub vertex_size: f64,
+ pub font_size: f64,
+}
+
+/// Calculate the optimal position for a vertex label with pre-cached collision data and spatial grid
pub fn calculate_optimal_label_position_cached(
component: &Component,
- map_width: f64,
- map_height: f64,
- vertex_size: f64,
- ctx: &cairo::Context,
- font_size: f64,
+ dimensions: &PositioningDimensions,
+ context: &Context,
all_lines: &[Line],
+ spatial_grid: &SpatialGrid,
note_rects: &[Rect],
-) -> Result<LabelPosition, cairo::Error> {
+) -> Result<LabelPosition, Error> {
+ let map_width = dimensions.map_size.0;
+ let map_height = dimensions.map_size.1;
let vertex_pixel_x = percent_to_pixel(component.coordinates.0, map_width);
let vertex_pixel_y = percent_to_pixel(component.coordinates.1, map_height);
// Calculate label size
- ctx.set_font_size(font_size);
- let extents = ctx.text_extents(&component.label)?;
+ context.set_font_size(dimensions.font_size);
+ let extents = context.text_extents(&component.label)?;
let label_width = extents.width() + 4.0;
let label_height = extents.height() + 2.0;
@@ -32,7 +41,7 @@ pub fn calculate_optimal_label_position_cached(
let candidates = generate_candidate_positions(
vertex_pixel_x,
vertex_pixel_y,
- vertex_size,
+ dimensions.vertex_size,
label_width,
label_height,
);
@@ -46,7 +55,7 @@ pub fn calculate_optimal_label_position_cached(
label_height,
);
- if count_line_collisions(&default_rect, all_lines) == 0
+ if count_line_collisions_with_grid(&default_rect, all_lines, spatial_grid) == 0
&& count_note_collisions(&default_rect, note_rects) == 0
{
return Ok(default_position.clone());
@@ -59,14 +68,14 @@ pub fn calculate_optimal_label_position_cached(
for (index, position) in candidates.iter().enumerate() {
let label_rect = Rect::new(position.x, position.y, label_width, label_height);
- let collision_count = count_line_collisions(&label_rect, all_lines);
+ let collision_count = count_line_collisions_with_grid(&label_rect, all_lines, spatial_grid);
let note_collision_count = count_note_collisions(&label_rect, note_rects);
// Distance from label center to vertex center
let label_center_x = position.x + label_width / 2.0;
let label_center_y = position.y + label_height / 2.0;
- let vertex_center_x = vertex_pixel_x + vertex_size / 2.0;
- let vertex_center_y = vertex_pixel_y + vertex_size / 2.0;
+ let vertex_center_x = vertex_pixel_x + dimensions.vertex_size / 2.0;
+ let vertex_center_y = vertex_pixel_y + dimensions.vertex_size / 2.0;
let distance = ((label_center_x - vertex_center_x).powi(2)
+ (label_center_y - vertex_center_y).powi(2))
.sqrt();
@@ -76,7 +85,7 @@ pub fn calculate_optimal_label_position_cached(
&label_rect,
vertex_pixel_x,
vertex_pixel_y,
- vertex_size,
+ dimensions.vertex_size,
);
// Score: collisions * 100 + note_collisions * 500 + distance * 0.5 + index * 0.1 + ownership
@@ -95,7 +104,6 @@ pub fn calculate_optimal_label_position_cached(
Ok(best_position)
}
-
/// Generate 8 candidate positions around a vertex
fn generate_candidate_positions(
vertex_x: f64,
@@ -156,7 +164,7 @@ pub fn collect_all_lines(
map_width: f64,
map_height: f64,
vertex_size: f64,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Vec<Line> {
let mut lines = Vec::new();
@@ -216,24 +224,38 @@ pub fn collect_all_lines(
lines
}
+/// Build a spatial grid from a collection of lines for fast collision detection
+pub fn build_spatial_grid(lines: &[Line], map_width: f64, map_height: f64) -> SpatialGrid {
+ // Use a cell size that balances grid overhead with query efficiency
+ // For typical maps, 100x100 pixel cells work well
+ let cell_size = 100.0;
+ let mut grid = SpatialGrid::new(map_width, map_height, cell_size);
+
+ for (idx, line) in lines.iter().enumerate() {
+ grid.insert_line(idx, line);
+ }
+
+ grid
+}
+
/// Collect rectangles for all notes
pub fn collect_note_rects(
map: &Map,
map_width: f64,
map_height: f64,
- ctx: &cairo::Context,
+ context: &Context,
font_size: f64,
-) -> Result<Vec<Rect>, cairo::Error> {
+) -> Result<Vec<Rect>, Error> {
let mut rects = Vec::new();
- ctx.set_font_size(font_size);
+ context.set_font_size(font_size);
for note in &map.notes {
let x = percent_to_pixel(note.coordinates.0, map_width);
let y = percent_to_pixel(note.coordinates.1, map_height);
// Calculate note size (simplified - just use text extents)
- let extents = ctx.text_extents(&note.text)?;
+ let extents = context.text_extents(&note.text)?;
let width = extents.width() + 10.0; // padding
let height = extents.height() + 10.0; // padding
@@ -243,11 +265,15 @@ pub fn collect_note_rects(
Ok(rects)
}
-/// Count how many lines intersect with a label rectangle
-fn count_line_collisions(label_rect: &Rect, lines: &[Line]) -> usize {
- lines
- .iter()
- .filter(|line| crate::geometry::line_intersects_rect(line, label_rect))
+/// Count how many lines intersect with a label rectangle using spatial grid
+fn count_line_collisions_with_grid(
+ label_rect: &Rect,
+ lines: &[Line],
+ spatial_grid: &SpatialGrid,
+) -> usize {
+ spatial_grid
+ .query_rect(label_rect)
+ .filter(|&idx| crate::geometry::line_intersects_rect(&lines[idx], label_rect))
.count()
}
@@ -272,8 +298,9 @@ fn calculate_ownership_penalty_fast(
let vertex_center_x = vertex_x + vertex_size / 2.0;
let vertex_center_y = vertex_y + vertex_size / 2.0;
- let distance =
- ((label_center_x - vertex_center_x).powi(2) + (label_center_y - vertex_center_y).powi(2)).sqrt();
+ let distance = ((label_center_x - vertex_center_x).powi(2)
+ + (label_center_y - vertex_center_y).powi(2))
+ .sqrt();
// Apply penalty if label is too far from its vertex
if distance > 100.0 {
diff --git a/src/stage_type.rs b/src/stage_type.rs
index f3ff056..a003317 100644
--- a/src/stage_type.rs
+++ b/src/stage_type.rs
@@ -28,6 +28,7 @@ pub enum StageType {
impl StageType {
/// Returns the name of this stage type
+ #[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Activities => "Activities",
@@ -56,6 +57,7 @@ impl StageType {
}
/// Returns the four evolution stage labels for this stage type
+ #[must_use]
pub fn stages(&self) -> [&'static str; 4] {
match self {
Self::Activities => [
diff --git a/src/utils.rs b/src/utils.rs
index b6738d4..8f6699a 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -4,26 +4,6 @@ pub fn percent_to_pixel(percentage: f64, dimension: f64) -> f64 {
(percentage * dimension) / 100.0
}
-/// Parses a hex color string to RGB components (0.0-1.0)
-pub fn parse_color(color: &str) -> (f64, f64, f64) {
- let color = color.trim_start_matches('#');
- if color.len() != 6 {
- return (0.0, 0.0, 0.0); // Default to black on error
- }
-
- let r = u8::from_str_radix(&color[0..2], 16).unwrap_or(0) as f64 / 255.0;
- let g = u8::from_str_radix(&color[2..4], 16).unwrap_or(0) as f64 / 255.0;
- let b = u8::from_str_radix(&color[4..6], 16).unwrap_or(0) as f64 / 255.0;
-
- (r, g, b)
-}
-
-/// Sets the cairo context color from a hex string
-pub fn set_color(ctx: &cairo::Context, color: &str) {
- let (r, g, b) = parse_color(color);
- ctx.set_source_rgb(r, g, b);
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -35,19 +15,4 @@ mod tests {
assert_eq!(percent_to_pixel(100.0, 100.0), 100.0);
assert_eq!(percent_to_pixel(25.0, 1300.0), 325.0);
}
-
- #[test]
- fn test_parse_color() {
- // Test white
- assert_eq!(parse_color("#FFFFFF"), (1.0, 1.0, 1.0));
-
- // Test black
- assert_eq!(parse_color("#000000"), (0.0, 0.0, 0.0));
-
- // Test custom color (#0F261F)
- let (r, g, b) = parse_color("#0F261F");
- assert!((r - 0.058823).abs() < 0.001); // 15/255
- assert!((g - 0.149019).abs() < 0.001); // 38/255
- assert!((b - 0.121568).abs() < 0.001); // 31/255
- }
}