aboutsummaryrefslogtreecommitdiff
path: root/src/smart_positioning.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/smart_positioning.rs')
-rw-r--r--src/smart_positioning.rs316
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, &note_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, &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 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(&note.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
+}