1use std::f64::consts::PI;
2use std::fs::read_to_string;
3use std::io::Cursor;
4
5use cairo::{
6 Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface,
7};
8use geo::{ConcaveHull, LineString, Point};
9use rustc_hash::FxHashMap;
10use tempfile::NamedTempFile;
11use wmap_parser::{Map, Stage};
12
13use crate::configuration::Configuration;
14use crate::error::RenderError;
15use crate::patterns::{SHADOW_GRID, SHINGLES, STITCH, WICKER, create_pattern};
16use crate::shapes::draw_vertex;
17use crate::smart_positioning::{
18 PositioningDimensions, build_spatial_grid, calculate_optimal_label_position_cached,
19 collect_all_lines, collect_note_rects,
20};
21use crate::stage_type::StageType;
22use crate::utils::percent_to_pixel;
23
24pub fn render_to_png(
40 map: &Map,
41 stage_type: StageType,
42 configuration: &Configuration,
43) -> Result<Vec<u8>, RenderError> {
44 #[allow(clippy::cast_possible_truncation)]
47 let total_width = (configuration.theme.sizes.map_width
48 + configuration.theme.sizes.padding * 2.0)
49 .round() as i32;
50 #[allow(clippy::cast_possible_truncation)]
51 let total_height = (configuration.theme.sizes.map_height
52 + configuration.theme.sizes.padding
53 + configuration.theme.sizes.bottom_padding)
54 .round() as i32;
55
56 let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?;
57 let context = Context::new(&surface)?;
58
59 render(&context, map, stage_type, configuration)?;
60
61 let mut buffer = Cursor::new(Vec::new());
62 surface.write_to_png(&mut buffer)?;
63 Ok(buffer.into_inner())
64}
65
66pub fn render_to_surface(
82 map: &Map,
83 stage_type: StageType,
84 configuration: &Configuration,
85) -> Result<ImageSurface, RenderError> {
86 #[allow(clippy::cast_possible_truncation)]
89 let total_width = (configuration.theme.sizes.map_width
90 + configuration.theme.sizes.padding * 2.0)
91 .round() as i32;
92 #[allow(clippy::cast_possible_truncation)]
93 let total_height = (configuration.theme.sizes.map_height
94 + configuration.theme.sizes.padding
95 + configuration.theme.sizes.bottom_padding)
96 .round() as i32;
97
98 let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?;
99 let context = Context::new(&surface)?;
100
101 render(&context, map, stage_type, configuration)?;
102
103 Ok(surface)
104}
105
106pub fn render_to_svg(
122 map: &Map,
123 stage_type: StageType,
124 configuration: &Configuration,
125) -> Result<String, RenderError> {
126 let total_width = configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0;
127 let total_height = configuration.theme.sizes.map_height
128 + configuration.theme.sizes.padding
129 + configuration.theme.sizes.bottom_padding;
130
131 let temp_file = NamedTempFile::new()?;
132 let path = temp_file
133 .path()
134 .to_str()
135 .ok_or_else(|| RenderError::InvalidConfig("Failed to create temp file path".to_string()))?;
136
137 let surface = SvgSurface::new(total_width, total_height, Some(path))?;
138 let context = Context::new(&surface)?;
139
140 render(&context, map, stage_type, configuration)?;
141
142 drop(context);
143 surface.finish();
144
145 let svg_content = read_to_string(path)?;
146 Ok(svg_content)
147}
148
149fn render(
151 context: &Context,
152 map: &Map,
153 stage_type: StageType,
154 configuration: &Configuration,
155) -> Result<(), RenderError> {
156 context.save()?;
158 context.translate(
159 configuration.theme.sizes.padding,
160 configuration.theme.sizes.padding,
161 );
162
163 let mut component_map: FxHashMap<String, (f64, f64)> =
166 FxHashMap::with_capacity_and_hasher(map.components.len(), rustc_hash::FxBuildHasher);
167 for component in &map.components {
168 let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
169 let y = percent_to_pixel(
170 component.coordinates.1,
171 configuration.theme.sizes.map_height,
172 );
173 component_map.insert(component.label.to_lowercase(), (x, y));
174 }
175
176 draw_background(context, configuration)?;
178
179 if configuration.options.show_background {
180 draw_stage_patterns(context, stage_type, map, configuration)?;
181 }
182
183 draw_axes(context, stage_type, map, configuration)?;
184 draw_stage_dividers(context, map, configuration)?;
185 draw_dependencies(context, map, configuration, &component_map)?;
186 draw_groups(context, map, configuration, &component_map)?;
187 draw_vertices(context, map, configuration)?;
188 draw_vertex_labels(context, map, configuration, &component_map)?;
189 draw_inertias(context, map, configuration, &component_map)?;
190 draw_evolutions(context, map, configuration, &component_map)?;
191 draw_notes(context, map, configuration)?;
192
193 context.restore()?;
194 Ok(())
195}
196
197fn draw_background(context: &Context, configuration: &Configuration) -> Result<(), RenderError> {
198 context.save()?;
199 let (r, g, b) = configuration.theme.colors.background;
200 context.set_source_rgb(r, g, b);
201 context.paint()?;
202 context.restore()?;
203 Ok(())
204}
205
206fn draw_stage_patterns(
207 context: &Context,
208 _stage_type: StageType,
209 map: &Map,
210 configuration: &Configuration,
211) -> Result<(), RenderError> {
212 let stages = get_stage_positions(map, configuration);
213
214 let pattern0 = create_pattern(
216 &STITCH,
217 configuration.theme.colors.stage_foreground,
218 configuration.theme.colors.stage_background,
219 )?;
220 let pattern1 = create_pattern(
221 &SHINGLES,
222 configuration.theme.colors.stage_foreground,
223 configuration.theme.colors.stage_background,
224 )?;
225 let pattern2 = create_pattern(
226 &SHADOW_GRID,
227 configuration.theme.colors.stage_foreground,
228 configuration.theme.colors.stage_background,
229 )?;
230 let pattern3 = create_pattern(
231 &WICKER,
232 configuration.theme.colors.stage_foreground,
233 configuration.theme.colors.stage_background,
234 )?;
235
236 context.save()?;
237
238 context.rectangle(0.0, 0.0, stages[0], configuration.theme.sizes.map_height);
240 context.set_source(&pattern0)?;
241 context.fill()?;
242
243 context.rectangle(
245 stages[0],
246 0.0,
247 stages[1] - stages[0],
248 configuration.theme.sizes.map_height,
249 );
250 context.set_source(&pattern1)?;
251 context.fill()?;
252
253 context.rectangle(
255 stages[1],
256 0.0,
257 stages[2] - stages[1],
258 configuration.theme.sizes.map_height,
259 );
260 context.set_source(&pattern2)?;
261 context.fill()?;
262
263 context.rectangle(
265 stages[2],
266 0.0,
267 configuration.theme.sizes.map_width - stages[2],
268 configuration.theme.sizes.map_height,
269 );
270 context.set_source(&pattern3)?;
271 context.fill()?;
272
273 context.restore()?;
274 Ok(())
275}
276
277fn draw_axes(
278 context: &Context,
279 stage_type: StageType,
280 map: &Map,
281 configuration: &Configuration,
282) -> Result<(), RenderError> {
283 let sizes = &configuration.theme.sizes;
284 let fonts = &configuration.theme.fonts;
285
286 context.save()?;
287 let (red, green, blue) = configuration.theme.colors.axis;
288 context.set_source_rgb(red, green, blue);
289 context.set_line_width(sizes.line_width * 2.0);
290
291 context.move_to(0.0, 0.0);
293 context.line_to(0.0, sizes.map_height);
294 context.line_to(sizes.map_width, sizes.map_height);
295 context.stroke()?;
296
297 let (red, green, blue) = configuration.theme.colors.label;
299 context.set_source_rgb(red, green, blue);
300 context.select_font_face(
301 &configuration.theme.fonts.face,
302 FontSlant::Normal,
303 FontWeight::Normal,
304 );
305 context.set_font_size(fonts.axis_label);
306 context.set_line_width(1.0);
307
308 context.save()?;
310 context.translate(-20.0, 45.0);
311 context.rotate(-PI / 2.0);
312 context.move_to(0.0, 0.0);
313 context.show_text("Visible")?;
314 context.restore()?;
315
316 context.save()?;
317 context.translate(-20.0, sizes.map_height);
318 context.rotate(-PI / 2.0);
319 context.move_to(0.0, 0.0);
320 context.show_text("Invisible")?;
321 context.restore()?;
322
323 let stage_height = sizes.stage_height;
325 let text_offset_y = -stage_height / 4.0 + sizes.vertex_size / 2.0;
326 context.move_to(0.0, text_offset_y);
327 context.show_text("Uncharted")?;
328
329 let text_ext = context.text_extents("Industrialised")?;
330 context.move_to(sizes.map_width - text_ext.width(), text_offset_y);
331 context.show_text("Industrialised")?;
332
333 let stage_labels = stage_type.stages();
335 let stages = get_stage_positions(map, configuration);
336 let padding = 5.0; let stage_label_y = sizes.map_height + padding + sizes.vertex_size / 2.0;
338 let line_height = fonts.axis_label * 1.2; let stage_widths = [
342 stages[0],
343 stages[1] - stages[0],
344 stages[2] - stages[1],
345 sizes.map_width - stages[2],
346 ];
347
348 for (i, &label) in stage_labels.iter().enumerate() {
350 let stage_x = match i {
351 0 => 0.0,
352 1 => stages[0],
353 2 => stages[1],
354 3 => stages[2],
355 _ => continue,
356 };
357
358 let wrapped_lines = wrap_text(context, label, stage_widths[i])?;
359
360 for (line_index, line) in wrapped_lines.iter().enumerate() {
361 #[allow(clippy::cast_precision_loss)]
363 let text_y = stage_label_y + line_index as f64 * line_height;
364 context.move_to(stage_x, text_y);
365 context.show_text(line)?;
366 }
367 }
368
369 context.restore()?;
370 Ok(())
371}
372
373fn draw_stage_dividers(
374 context: &Context,
375 map: &Map,
376 configuration: &Configuration,
377) -> Result<(), RenderError> {
378 let stages = get_stage_positions(map, configuration);
379
380 context.save()?;
381 let (r, g, b) = configuration.theme.colors.axis;
382 context.set_source_rgb(r, g, b);
383 context.set_line_width(configuration.theme.sizes.line_width * 2.0);
384 context.set_dash(&[10.0, 18.0], 0.0);
385
386 for &stage_x in &stages {
387 context.move_to(stage_x, 0.0);
388 context.line_to(stage_x, configuration.theme.sizes.map_height);
389 }
390
391 context.stroke()?;
392 context.restore()?;
393 Ok(())
394}
395
396fn draw_dependencies(
397 context: &Context,
398 map: &Map,
399 configuration: &Configuration,
400 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
401) -> Result<(), RenderError> {
402 context.save()?;
403 let (r, g, b) = configuration.theme.colors.vertex;
404 context.set_source_rgb(r, g, b);
405 context.set_line_width(configuration.theme.sizes.line_width);
406
407 let half_width = configuration.theme.sizes.vertex_size / 2.0;
408 let half_height = configuration.theme.sizes.vertex_size / 2.0;
409 let arrow_angle = PI / 4.0; for dependency in &map.dependencies {
412 let Some(from_coords) = component_map.get(&dependency.from.to_lowercase()) else {
413 continue;
414 };
415 let Some(to_coords) = component_map.get(&dependency.to.to_lowercase()) else {
416 continue;
417 };
418
419 let origin_x = from_coords.0 + half_width;
420 let origin_y = from_coords.1 + half_height;
421 let dest_x = to_coords.0 + half_width;
422 let dest_y = to_coords.1 + half_height;
423
424 let angle = (dest_y - origin_y).atan2(dest_x - origin_x);
425 let cosine = angle.cos();
426 let sine = angle.sin();
427
428 let offset_origin_x = origin_x + half_width * cosine;
430 let offset_origin_y = origin_y + half_height * sine;
431 let offset_dest_x = dest_x - half_width * cosine;
432 let offset_dest_y = dest_y - half_height * sine;
433
434 context.move_to(offset_origin_x, offset_origin_y);
436 context.line_to(offset_dest_x, offset_dest_y);
437
438 if dependency.is_directed {
440 let upper_angle = angle - arrow_angle;
441 let lower_angle = angle + arrow_angle;
442 let arrowhead_size = configuration.theme.sizes.arrowhead_size;
443
444 context.move_to(
445 offset_dest_x - arrowhead_size * upper_angle.cos(),
446 offset_dest_y - arrowhead_size * upper_angle.sin(),
447 );
448 context.line_to(offset_dest_x, offset_dest_y);
449 context.line_to(
450 offset_dest_x - arrowhead_size * lower_angle.cos(),
451 offset_dest_y - arrowhead_size * lower_angle.sin(),
452 );
453 }
454 }
455
456 context.stroke()?;
458
459 context.restore()?;
460 Ok(())
461}
462
463fn draw_groups(
464 context: &Context,
465 map: &Map,
466 configuration: &Configuration,
467 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
468) -> Result<(), RenderError> {
469 if map.groups.is_empty() {
470 return Ok(());
471 }
472
473 #[allow(clippy::cast_possible_truncation)]
476 let width =
477 (configuration.theme.sizes.map_width + configuration.theme.sizes.vertex_size) as i32;
478 #[allow(clippy::cast_possible_truncation)]
479 let height =
480 (configuration.theme.sizes.map_height + configuration.theme.sizes.vertex_size) as i32;
481 let offscreen = ImageSurface::create(Format::ARgb32, width, height)?;
482 let offscreen_context = Context::new(&offscreen)?;
483
484 offscreen_context.translate(
486 configuration.theme.sizes.vertex_size / 2.0,
487 configuration.theme.sizes.vertex_size / 2.0,
488 );
489
490 for (group_index, group) in map.groups.iter().enumerate() {
491 let component_coords: Vec<(f64, f64)> = group
492 .components
493 .iter()
494 .filter_map(|label| component_map.get(&label.to_lowercase()).copied())
495 .collect();
496
497 if component_coords.is_empty() {
498 continue;
499 }
500
501 let color = &configuration.theme.colors.groups
502 [group_index % configuration.theme.colors.groups.len()];
503 let is_line = component_coords.len() == 2;
504
505 let coords = if component_coords.len() == 1 {
506 let (x, y) = component_coords[0];
508 let radius = 3.0;
509 let segments = 16;
510 let mut circle_coords = Vec::new();
511 for i in 0..=segments {
512 let angle = (f64::from(i) / f64::from(segments)) * 2.0 * PI;
513 circle_coords.push((x + radius * angle.cos(), y + radius * angle.sin()));
514 }
515 circle_coords
516 } else if is_line {
517 component_coords
519 } else {
520 let points: Vec<Point> = component_coords
522 .iter()
523 .map(|&(x, y)| Point::new(x, y))
524 .collect();
525
526 let line_string: LineString = points.into();
527 let hull = line_string.concave_hull();
528 hull.exterior().coords().map(|c| (c.x, c.y)).collect()
529 };
530
531 if coords.is_empty() {
532 continue;
533 }
534
535 let (r, g, b) = *color;
537 offscreen_context.set_source_rgb(r, g, b);
538 offscreen_context.move_to(coords[0].0, coords[0].1);
539 for &(x, y) in &coords[1..] {
540 offscreen_context.line_to(x, y);
541 }
542
543 if !is_line {
545 offscreen_context.close_path();
546 offscreen_context.fill_preserve()?;
547 }
548
549 offscreen_context.set_line_width(configuration.theme.sizes.vertex_size * 1.75);
551 offscreen_context.set_line_cap(LineCap::Round);
552 offscreen_context.set_line_join(LineJoin::Round);
553 offscreen_context.stroke()?;
554 }
555
556 context.save()?;
558 context.set_source_surface(&offscreen, 0.0, 0.0)?;
559 context.paint_with_alpha(configuration.theme.opacity.groups)?;
560 context.restore()?;
561
562 Ok(())
563}
564
565fn draw_vertices(
566 context: &Context,
567 map: &Map,
568 configuration: &Configuration,
569) -> Result<(), RenderError> {
570 context.save()?;
571 let (red, green, blue) = configuration.theme.colors.vertex;
572 context.set_source_rgb(red, green, blue);
573
574 for component in &map.components {
575 let vertex_x =
576 percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
577 let vertex_y = percent_to_pixel(
578 component.coordinates.1,
579 configuration.theme.sizes.map_height,
580 );
581
582 draw_vertex(
583 context,
584 &component.shape,
585 vertex_x,
586 vertex_y,
587 configuration.theme.sizes.vertex_size,
588 configuration.theme.sizes.vertex_size,
589 )?;
590 }
591
592 context.restore()?;
593 Ok(())
594}
595
596fn draw_vertex_labels(
597 context: &Context,
598 map: &Map,
599 configuration: &Configuration,
600 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
601) -> Result<(), RenderError> {
602 context.save()?;
603 let (red, green, blue) = configuration.theme.colors.label;
604 context.set_source_rgb(red, green, blue);
605 context.select_font_face(
606 &configuration.theme.fonts.face,
607 FontSlant::Normal,
608 FontWeight::Normal,
609 );
610 context.set_font_size(configuration.theme.fonts.vertex_label);
611
612 if configuration.options.smart_label_positioning {
613 let has_significant_complexity = map.dependencies.len() + map.evolutions.len() > 10;
615
616 if has_significant_complexity {
617 let all_lines = collect_all_lines(
620 map,
621 configuration.theme.sizes.map_width,
622 configuration.theme.sizes.map_height,
623 configuration.theme.sizes.vertex_size,
624 component_map,
625 );
626
627 let spatial_grid = build_spatial_grid(
628 &all_lines,
629 configuration.theme.sizes.map_width,
630 configuration.theme.sizes.map_height,
631 );
632
633 let note_rects = collect_note_rects(
634 map,
635 configuration.theme.sizes.map_width,
636 configuration.theme.sizes.map_height,
637 context,
638 configuration.theme.fonts.vertex_label,
639 )?;
640
641 for component in &map.components {
642 let position = calculate_optimal_label_position_cached(
643 component,
644 &PositioningDimensions {
645 map_size: (
646 configuration.theme.sizes.map_width,
647 configuration.theme.sizes.map_height,
648 ),
649 vertex_size: configuration.theme.sizes.vertex_size,
650 font_size: configuration.theme.fonts.vertex_label,
651 },
652 context,
653 &all_lines,
654 &spatial_grid,
655 ¬e_rects,
656 )?;
657
658 let label_x = position.x;
659 let label_y = position.y + 7.0; context.move_to(label_x, label_y);
662 context.show_text(&component.label)?;
663 }
664 } else {
665 for component in &map.components {
667 let vertex_x =
668 percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
669 let vertex_y = percent_to_pixel(
670 component.coordinates.1,
671 configuration.theme.sizes.map_height,
672 );
673 let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0;
674 let label_y = vertex_y
675 + configuration.theme.sizes.vertex_size / 2.0
676 + configuration.theme.fonts.vertex_label / 3.0;
677
678 context.move_to(label_x, label_y);
679 context.show_text(&component.label)?;
680 }
681 }
682 } else {
683 for component in &map.components {
685 let vertex_x =
686 percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
687 let vertex_y = percent_to_pixel(
688 component.coordinates.1,
689 configuration.theme.sizes.map_height,
690 );
691 let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0;
692 let label_y = vertex_y
693 + configuration.theme.sizes.vertex_size / 2.0
694 + configuration.theme.fonts.vertex_label / 3.0;
695
696 context.move_to(label_x, label_y);
697 context.show_text(&component.label)?;
698 }
699 }
700
701 context.restore()?;
702 Ok(())
703}
704
705fn draw_inertias(
706 context: &Context,
707 map: &Map,
708 configuration: &Configuration,
709 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
710) -> Result<(), RenderError> {
711 context.save()?;
712 let (red, green, blue) = configuration.theme.colors.inertia;
713 context.set_source_rgb(red, green, blue);
714
715 let corner_radius = 2.0;
716 let rect_width = configuration.theme.sizes.vertex_size / 2.0;
717 let rect_height = configuration.theme.sizes.vertex_size * 2.0;
718
719 for inertia in &map.inertias {
720 if let Some(&(origin_x, origin_y)) = component_map.get(&inertia.component.to_lowercase()) {
721 let inertia_x = origin_x + 3.0 * configuration.theme.sizes.vertex_size;
723 let inertia_y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0;
724
725 draw_rounded_rect(
727 context,
728 inertia_x,
729 inertia_y,
730 rect_width,
731 rect_height,
732 corner_radius,
733 );
734 context.fill()?;
735 }
736 }
737
738 context.restore()?;
739 Ok(())
740}
741
742fn draw_rounded_rect(context: &Context, x: f64, y: f64, width: f64, height: f64, radius: f64) {
743 let r = radius;
744 context.new_path();
745 context.arc(
746 x + r,
747 y + r,
748 r,
749 std::f64::consts::PI,
750 3.0 * std::f64::consts::PI / 2.0,
751 );
752 context.arc(
753 x + width - r,
754 y + r,
755 r,
756 3.0 * std::f64::consts::PI / 2.0,
757 0.0,
758 );
759 context.arc(
760 x + width - r,
761 y + height - r,
762 r,
763 0.0,
764 std::f64::consts::PI / 2.0,
765 );
766 context.arc(
767 x + r,
768 y + height - r,
769 r,
770 std::f64::consts::PI / 2.0,
771 std::f64::consts::PI,
772 );
773 context.close_path();
774}
775
776fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result<Vec<String>, Error> {
778 let words: Vec<&str> = text.split_whitespace().collect();
779 let mut lines = Vec::new();
780 let mut current_line = String::new();
781
782 for word in words {
783 let test_line = if current_line.is_empty() {
784 word.to_string()
785 } else {
786 format!("{current_line} {word}")
787 };
788
789 let extents = context.text_extents(&test_line)?;
790
791 if extents.width() <= max_width {
792 current_line = test_line;
793 } else {
794 if !current_line.is_empty() {
795 lines.push(current_line);
796 }
797 current_line = word.to_string();
798 }
799 }
800
801 if !current_line.is_empty() {
802 lines.push(current_line);
803 }
804
805 if lines.is_empty() {
807 lines.push(text.to_string());
808 }
809
810 Ok(lines)
811}
812
813fn draw_evolutions(
814 context: &Context,
815 map: &Map,
816 configuration: &Configuration,
817 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
818) -> Result<(), RenderError> {
819 context.save()?;
820 let (r, g, b) = configuration.theme.colors.evolution;
821 context.set_source_rgb(r, g, b);
822 context.set_line_width(configuration.theme.sizes.line_width * 2.0);
823 context.set_dash(&[10.0], 0.0);
824
825 for evolution in &map.evolutions {
826 if let Some(&(origin_x, origin_y)) = component_map.get(&evolution.component.to_lowercase())
827 {
828 let evolution_offset =
830 percent_to_pixel(evolution.value, configuration.theme.sizes.map_width);
831 let dest_x = (origin_x + evolution_offset)
832 .max(0.0)
833 .min(configuration.theme.sizes.map_width);
834 let dest_y = origin_y;
835
836 let multiplier = if dest_x > origin_x { 1.0 } else { -1.0 };
838
839 let offset_origin_x = origin_x
841 + multiplier * (configuration.theme.sizes.vertex_size / 2.0)
842 + configuration.theme.sizes.vertex_size / 2.0;
843 let offset_origin_y = origin_y + configuration.theme.sizes.vertex_size / 2.0;
844 let offset_dest_x = dest_x - multiplier * (configuration.theme.sizes.vertex_size / 2.0)
845 + configuration.theme.sizes.vertex_size / 2.0;
846 let offset_dest_y = dest_y + configuration.theme.sizes.vertex_size / 2.0;
847
848 context.move_to(offset_origin_x, offset_origin_y);
850 context.line_to(offset_dest_x, offset_dest_y);
851
852 let upper_angle = -PI / 4.0;
854 let lower_angle = PI / 4.0;
855 let arrowhead_size = configuration.theme.sizes.arrowhead_size;
856
857 context.move_to(offset_dest_x, offset_dest_y);
858 context.line_to(
859 offset_dest_x - multiplier * arrowhead_size * upper_angle.cos(),
860 offset_dest_y - multiplier * arrowhead_size * upper_angle.sin(),
861 );
862
863 context.move_to(offset_dest_x, offset_dest_y);
864 context.line_to(
865 offset_dest_x - multiplier * arrowhead_size * lower_angle.cos(),
866 offset_dest_y - multiplier * arrowhead_size * lower_angle.sin(),
867 );
868 }
869 }
870
871 context.stroke()?;
873
874 context.restore()?;
875 Ok(())
876}
877
878fn draw_notes(
879 context: &Context,
880 map: &Map,
881 configuration: &Configuration,
882) -> Result<(), RenderError> {
883 let padding = 5.0;
884 let max_width = 400.0;
885
886 context.save()?;
887 context.select_font_face(
888 &configuration.theme.fonts.face,
889 FontSlant::Normal,
890 FontWeight::Normal,
891 );
892 context.set_font_size(configuration.theme.fonts.note);
893
894 for note in &map.notes {
895 let note_x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width);
896 let note_y = percent_to_pixel(note.coordinates.1, configuration.theme.sizes.map_height);
897
898 let text = note.text.replace("\\n", "\n");
900 let lines: Vec<&str> = text.split('\n').collect();
901
902 let mut widest_line = 0.0;
904 for line in &lines {
905 let extents = context.text_extents(line)?;
906 if extents.width() > widest_line {
907 widest_line = extents.width();
908 }
909 }
910
911 let box_width = f64::min(max_width, widest_line + padding * 2.0);
912 #[allow(clippy::cast_precision_loss)]
914 let box_height = lines.len() as f64 * configuration.theme.line_heights.note + padding * 2.0;
915
916 let (red, green, blue) = configuration.theme.colors.background;
918 context.set_source_rgb(red, green, blue);
919 context.rectangle(note_x, note_y, box_width, box_height);
920 context.fill()?;
921
922 let (red, green, blue) = configuration.theme.colors.vertex;
924 context.set_source_rgb(red, green, blue);
925 context.set_line_width(configuration.theme.sizes.line_width);
926 context.rectangle(note_x, note_y, box_width, box_height);
927 context.stroke()?;
928
929 let (red, green, blue) = configuration.theme.colors.label;
931 context.set_source_rgb(red, green, blue);
932 for (i, line) in lines.iter().enumerate() {
933 #[allow(clippy::cast_precision_loss)]
935 let line_offset = i as f64 * configuration.theme.line_heights.note;
936 context.move_to(
937 note_x + padding,
938 note_y + padding + line_offset + configuration.theme.fonts.note,
939 );
940 context.show_text(line)?;
941 }
942 }
943
944 context.restore()?;
945 Ok(())
946}
947
948fn get_stage_positions(map: &Map, configuration: &Configuration) -> [f64; 3] {
950 let default_stages = [25.0, 50.0, 75.0];
951 let mut stages = default_stages;
952
953 for stage_data in &map.stages {
955 let index = match stage_data.stage {
956 Stage::I => 0,
957 Stage::Ii => 1,
958 Stage::Iii => 2,
959 Stage::Iv => continue, };
961 stages[index] = stage_data.value; }
963
964 [
965 percent_to_pixel(stages[0], configuration.theme.sizes.map_width),
966 percent_to_pixel(stages[1], configuration.theme.sizes.map_width),
967 percent_to_pixel(stages[2], configuration.theme.sizes.map_width),
968 ]
969}