use crate::geometry::{Line, Rect}; use crate::utils::percent_to_pixel; use wmap_parser::{Component, Map}; #[derive(Debug, Clone)] pub struct LabelPosition { pub x: f64, pub y: f64, } /// Calculate the optimal position for a vertex label to minimize collisions pub fn calculate_optimal_label_position( component: &Component, map: &Map, map_width: f64, map_height: f64, vertex_size: f64, ctx: &cairo::Context, font_size: f64, ) -> Result { 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)?; let label_width = extents.width() + 4.0; let label_height = extents.height() + 2.0; // Generate candidate positions let candidates = generate_candidate_positions( vertex_pixel_x, vertex_pixel_y, vertex_size, label_width, label_height, ); // Collect all lines (dependencies, evolutions, axis lines) let all_lines = collect_all_lines(map, map_width, map_height, vertex_size); // Collect note rectangles let note_rects = collect_note_rects(map, map_width, map_height, ctx, font_size)?; // Check if default position (right) has no collisions let default_position = &candidates[0]; let default_rect = Rect::new( default_position.x, default_position.y, label_width, label_height, ); if count_line_collisions(&default_rect, &all_lines) == 0 && count_note_collisions(&default_rect, ¬e_rects) == 0 { return Ok(default_position.clone()); } // Score all positions and find the best let mut best_position = default_position.clone(); let mut best_score = f64::INFINITY; 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 note_collision_count = count_note_collisions(&label_rect, ¬e_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 distance = ((label_center_x - vertex_center_x).powi(2) + (label_center_y - vertex_center_y).powi(2)) .sqrt(); // Calculate ownership penalty let ownership_penalty = calculate_ownership_penalty( &label_rect, component, &map.components, map_width, map_height, vertex_size, ); // Score: collisions * 100 + note_collisions * 500 + distance * 0.5 + index * 0.1 + ownership let score = (collision_count as f64) * 100.0 + (note_collision_count as f64) * 500.0 + distance * 0.5 + (index as f64) * 0.1 + ownership_penalty; if score < best_score { best_score = score; best_position = position.clone(); } } Ok(best_position) } /// Generate 8 candidate positions around a vertex fn generate_candidate_positions( vertex_x: f64, vertex_y: f64, vertex_size: f64, label_width: f64, label_height: f64, ) -> Vec { let padding = 5.0; vec![ // Right (default) LabelPosition { x: vertex_x + vertex_size + padding, y: vertex_y + (vertex_size - label_height) / 2.0, }, // Left LabelPosition { x: vertex_x - label_width - padding, y: vertex_y + (vertex_size - label_height) / 2.0, }, // Top LabelPosition { x: vertex_x + (vertex_size - label_width) / 2.0, y: vertex_y - label_height - padding, }, // Bottom LabelPosition { x: vertex_x + (vertex_size - label_width) / 2.0, y: vertex_y + vertex_size + padding, }, // Top-right LabelPosition { x: vertex_x + vertex_size + padding, y: vertex_y - label_height - padding, }, // Top-left LabelPosition { x: vertex_x - label_width - padding, y: vertex_y - label_height - padding, }, // Bottom-right LabelPosition { x: vertex_x + vertex_size + padding, y: vertex_y + vertex_size + padding, }, // Bottom-left LabelPosition { x: vertex_x - label_width - padding, y: vertex_y + vertex_size + padding, }, ] } /// Collect all lines from dependencies, evolutions, and axis lines fn collect_all_lines(map: &Map, map_width: f64, map_height: f64, vertex_size: f64) -> Vec { use std::collections::HashMap; let mut lines = Vec::new(); // Build component position map let mut component_map: HashMap = HashMap::new(); for component in &map.components { let x = percent_to_pixel(component.coordinates.0, map_width); let y = percent_to_pixel(component.coordinates.1, map_height); component_map.insert(component.label.to_lowercase(), (x, y)); } // Collect dependency lines for dependency in &map.dependencies { if let (Some(&origin), Some(&destination)) = ( component_map.get(&dependency.from.to_lowercase()), component_map.get(&dependency.to.to_lowercase()), ) { let slope = (destination.1 - origin.1) / (destination.0 - origin.0); let angle = slope.atan(); let multiplier = if slope < 0.0 { -1.0 } else { 1.0 }; let offset_origin_x = origin.0 + multiplier * (vertex_size / 2.0) * angle.cos(); let offset_origin_y = origin.1 + multiplier * (vertex_size / 2.0) * angle.sin(); let offset_dest_x = destination.0 - multiplier * (vertex_size / 2.0) * angle.cos(); let offset_dest_y = destination.1 - multiplier * (vertex_size / 2.0) * angle.sin(); let adjusted_origin_x = offset_origin_x + vertex_size / 2.0; let adjusted_origin_y = offset_origin_y + vertex_size / 2.0; let adjusted_dest_x = offset_dest_x + vertex_size / 2.0; let adjusted_dest_y = offset_dest_y + vertex_size / 2.0; lines.push(Line::new( (adjusted_origin_x, adjusted_origin_y), (adjusted_dest_x, adjusted_dest_y), )); } } // Collect evolution lines for evolution in &map.evolutions { if let Some(&origin) = component_map.get(&evolution.component.to_lowercase()) { let evolution_offset = percent_to_pixel(evolution.value, map_width); let destination_x = (origin.0 + evolution_offset).max(0.0).min(map_width); let destination_y = origin.1; let multiplier = if destination_x > origin.0 { 1.0 } else { -1.0 }; let offset_origin_x = origin.0 + multiplier * (vertex_size / 2.0) + vertex_size / 2.0; let offset_origin_y = origin.1 + vertex_size / 2.0; let offset_dest_x = destination_x - multiplier * (vertex_size / 2.0) + vertex_size / 2.0; let offset_dest_y = destination_y + vertex_size / 2.0; lines.push(Line::new( (offset_origin_x, offset_origin_y), (offset_dest_x, offset_dest_y), )); } } // Add axis lines lines.push(Line::new((0.0, 0.0), (0.0, map_height))); // Y-axis lines.push(Line::new((0.0, map_height), (map_width, map_height))); // X-axis lines } /// Collect rectangles for all notes fn collect_note_rects( map: &Map, map_width: f64, map_height: f64, ctx: &cairo::Context, font_size: f64, ) -> Result, cairo::Error> { let mut rects = Vec::new(); ctx.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(¬e.text)?; let width = extents.width() + 10.0; // padding let height = extents.height() + 10.0; // padding rects.push(Rect::new(x, y, width, height)); } 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() } /// Count how many note rectangles overlap with a label rectangle fn count_note_collisions(label_rect: &Rect, note_rects: &[Rect]) -> usize { note_rects .iter() .filter(|note_rect| crate::geometry::rect_intersects_rect(label_rect, note_rect)) .count() } /// Calculate ownership penalty if label is closer to another vertex fn calculate_ownership_penalty( label_rect: &Rect, own_component: &Component, all_components: &[Component], map_width: f64, map_height: f64, vertex_size: f64, ) -> f64 { if all_components.is_empty() { return 0.0; } let label_center_x = label_rect.x + label_rect.width / 2.0; let label_center_y = label_rect.y + label_rect.height / 2.0; let own_vertex_x = percent_to_pixel(own_component.coordinates.0, map_width); let own_vertex_y = percent_to_pixel(own_component.coordinates.1, map_height); let own_center_x = own_vertex_x + vertex_size / 2.0; let own_center_y = own_vertex_y + vertex_size / 2.0; let distance_to_own = ((label_center_x - own_center_x).powi(2) + (label_center_y - own_center_y).powi(2)).sqrt(); for other_component in all_components { if other_component.label == own_component.label { continue; } let other_vertex_x = percent_to_pixel(other_component.coordinates.0, map_width); let other_vertex_y = percent_to_pixel(other_component.coordinates.1, map_height); let other_center_x = other_vertex_x + vertex_size / 2.0; let other_center_y = other_vertex_y + vertex_size / 2.0; let distance_to_other = ((label_center_x - other_center_x).powi(2) + (label_center_y - other_center_y).powi(2)) .sqrt(); if distance_to_other < distance_to_own { return 50.0; } } 0.0 }