diff options
Diffstat (limited to 'src/smart_positioning.rs')
| -rw-r--r-- | src/smart_positioning.rs | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/src/smart_positioning.rs b/src/smart_positioning.rs new file mode 100644 index 0000000..2def897 --- /dev/null +++ b/src/smart_positioning.rs @@ -0,0 +1,316 @@ +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<LabelPosition, cairo::Error> { + 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<LabelPosition> { + 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<Line> { + use std::collections::HashMap; + + let mut lines = Vec::new(); + + // Build component position map + let mut component_map: HashMap<String, (f64, f64)> = 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<Vec<Rect>, 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 +} |