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, 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 map: &Map,
209 configuration: &Configuration,
210) -> Result<(), RenderError> {
211 let stages = get_stage_positions(map, configuration);
212
213 let pattern0 = create_pattern(
215 &STITCH,
216 configuration.theme.colors.stage_foreground,
217 configuration.theme.colors.stage_background,
218 )?;
219 let pattern1 = create_pattern(
220 &SHINGLES,
221 configuration.theme.colors.stage_foreground,
222 configuration.theme.colors.stage_background,
223 )?;
224 let pattern2 = create_pattern(
225 &SHADOW_GRID,
226 configuration.theme.colors.stage_foreground,
227 configuration.theme.colors.stage_background,
228 )?;
229 let pattern3 = create_pattern(
230 &WICKER,
231 configuration.theme.colors.stage_foreground,
232 configuration.theme.colors.stage_background,
233 )?;
234
235 context.save()?;
236
237 context.rectangle(0.0, 0.0, stages[0], configuration.theme.sizes.map_height);
239 context.set_source(&pattern0)?;
240 context.fill()?;
241
242 context.rectangle(
244 stages[0],
245 0.0,
246 stages[1] - stages[0],
247 configuration.theme.sizes.map_height,
248 );
249 context.set_source(&pattern1)?;
250 context.fill()?;
251
252 context.rectangle(
254 stages[1],
255 0.0,
256 stages[2] - stages[1],
257 configuration.theme.sizes.map_height,
258 );
259 context.set_source(&pattern2)?;
260 context.fill()?;
261
262 context.rectangle(
264 stages[2],
265 0.0,
266 configuration.theme.sizes.map_width - stages[2],
267 configuration.theme.sizes.map_height,
268 );
269 context.set_source(&pattern3)?;
270 context.fill()?;
271
272 context.restore()?;
273 Ok(())
274}
275
276fn draw_axes(
277 context: &Context,
278 stage_type: &StageType,
279 map: &Map,
280 configuration: &Configuration,
281) -> Result<(), RenderError> {
282 let sizes = &configuration.theme.sizes;
283 let fonts = &configuration.theme.fonts;
284
285 context.save()?;
286 let (red, green, blue) = configuration.theme.colors.axis;
287 context.set_source_rgb(red, green, blue);
288 context.set_line_width(sizes.line_width * 2.0);
289
290 context.move_to(0.0, 0.0);
292 context.line_to(0.0, sizes.map_height);
293 context.line_to(sizes.map_width, sizes.map_height);
294 context.stroke()?;
295
296 let (red, green, blue) = configuration.theme.colors.label;
298 context.set_source_rgb(red, green, blue);
299 context.select_font_face(
300 &configuration.theme.fonts.face,
301 FontSlant::Normal,
302 FontWeight::Normal,
303 );
304 context.set_font_size(fonts.axis_label);
305 context.set_line_width(1.0);
306
307 context.save()?;
309 context.translate(-20.0, 45.0);
310 context.rotate(-PI / 2.0);
311 context.move_to(0.0, 0.0);
312 context.show_text("Visible")?;
313 context.restore()?;
314
315 context.save()?;
316 context.translate(-20.0, sizes.map_height);
317 context.rotate(-PI / 2.0);
318 context.move_to(0.0, 0.0);
319 context.show_text("Invisible")?;
320 context.restore()?;
321
322 let stage_height = sizes.stage_height;
324 let text_offset_y = -stage_height / 4.0 + sizes.vertex_size / 2.0;
325 context.move_to(0.0, text_offset_y);
326 context.show_text("Uncharted")?;
327
328 let text_ext = context.text_extents("Industrialised")?;
329 context.move_to(sizes.map_width - text_ext.width(), text_offset_y);
330 context.show_text("Industrialised")?;
331
332 let stage_labels = stage_type.stages();
334 let stages = get_stage_positions(map, configuration);
335 let padding = 5.0; let stage_label_y = sizes.map_height + padding + sizes.vertex_size / 2.0;
337 let line_height = fonts.axis_label * 1.2; let stage_widths = [
341 stages[0],
342 stages[1] - stages[0],
343 stages[2] - stages[1],
344 sizes.map_width - stages[2],
345 ];
346
347 for (i, &label) in stage_labels.iter().enumerate() {
349 let stage_x = match i {
350 0 => 0.0,
351 1 => stages[0],
352 2 => stages[1],
353 3 => stages[2],
354 _ => continue,
355 };
356
357 let wrapped_lines = wrap_text(context, label, stage_widths[i])?;
358
359 for (line_index, line) in wrapped_lines.iter().enumerate() {
360 #[allow(clippy::cast_precision_loss)]
362 let text_y = stage_label_y + line_index as f64 * line_height;
363 context.move_to(stage_x, text_y);
364 context.show_text(line)?;
365 }
366 }
367
368 context.restore()?;
369 Ok(())
370}
371
372fn draw_stage_dividers(
373 context: &Context,
374 map: &Map,
375 configuration: &Configuration,
376) -> Result<(), RenderError> {
377 let stages = get_stage_positions(map, configuration);
378
379 context.save()?;
380 let (r, g, b) = configuration.theme.colors.axis;
381 context.set_source_rgb(r, g, b);
382 context.set_line_width(configuration.theme.sizes.line_width * 2.0);
383 context.set_dash(&[10.0, 18.0], 0.0);
384
385 for &stage_x in &stages {
386 context.move_to(stage_x, 0.0);
387 context.line_to(stage_x, configuration.theme.sizes.map_height);
388 }
389
390 context.stroke()?;
391 context.restore()?;
392 Ok(())
393}
394
395fn draw_dependencies(
396 context: &Context,
397 map: &Map,
398 configuration: &Configuration,
399 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
400) -> Result<(), RenderError> {
401 context.save()?;
402 let (r, g, b) = configuration.theme.colors.vertex;
403 context.set_source_rgb(r, g, b);
404 context.set_line_width(configuration.theme.sizes.line_width);
405
406 let half_width = configuration.theme.sizes.vertex_size / 2.0;
407 let half_height = configuration.theme.sizes.vertex_size / 2.0;
408 let arrow_angle = PI / 4.0; for dependency in &map.dependencies {
411 let Some(from_coords) = component_map.get(&dependency.from.to_lowercase()) else {
412 continue;
413 };
414 let Some(to_coords) = component_map.get(&dependency.to.to_lowercase()) else {
415 continue;
416 };
417
418 let origin_x = from_coords.0 + half_width;
419 let origin_y = from_coords.1 + half_height;
420 let dest_x = to_coords.0 + half_width;
421 let dest_y = to_coords.1 + half_height;
422
423 let angle = (dest_y - origin_y).atan2(dest_x - origin_x);
424 let cosine = angle.cos();
425 let sine = angle.sin();
426
427 let offset_origin_x = origin_x + half_width * cosine;
429 let offset_origin_y = origin_y + half_height * sine;
430 let offset_dest_x = dest_x - half_width * cosine;
431 let offset_dest_y = dest_y - half_height * sine;
432
433 context.move_to(offset_origin_x, offset_origin_y);
435 context.line_to(offset_dest_x, offset_dest_y);
436
437 if dependency.is_directed {
439 let upper_angle = angle - arrow_angle;
440 let lower_angle = angle + arrow_angle;
441 let arrowhead_size = configuration.theme.sizes.arrowhead_size;
442
443 context.move_to(
444 offset_dest_x - arrowhead_size * upper_angle.cos(),
445 offset_dest_y - arrowhead_size * upper_angle.sin(),
446 );
447 context.line_to(offset_dest_x, offset_dest_y);
448 context.line_to(
449 offset_dest_x - arrowhead_size * lower_angle.cos(),
450 offset_dest_y - arrowhead_size * lower_angle.sin(),
451 );
452 }
453 }
454
455 context.stroke()?;
457
458 context.restore()?;
459 Ok(())
460}
461
462fn draw_groups(
463 context: &Context,
464 map: &Map,
465 configuration: &Configuration,
466 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
467) -> Result<(), RenderError> {
468 if map.groups.is_empty() {
469 return Ok(());
470 }
471
472 #[allow(clippy::cast_possible_truncation)]
475 let width =
476 (configuration.theme.sizes.map_width + configuration.theme.sizes.vertex_size) as i32;
477 #[allow(clippy::cast_possible_truncation)]
478 let height =
479 (configuration.theme.sizes.map_height + configuration.theme.sizes.vertex_size) as i32;
480 let offscreen = ImageSurface::create(Format::ARgb32, width, height)?;
481 let offscreen_context = Context::new(&offscreen)?;
482
483 offscreen_context.translate(
485 configuration.theme.sizes.vertex_size / 2.0,
486 configuration.theme.sizes.vertex_size / 2.0,
487 );
488
489 for (group_index, group) in map.groups.iter().enumerate() {
490 let component_coords: Vec<(f64, f64)> = group
491 .components
492 .iter()
493 .filter_map(|label| component_map.get(&label.to_lowercase()).copied())
494 .collect();
495
496 if component_coords.is_empty() {
497 continue;
498 }
499
500 let color = &configuration.theme.colors.groups
501 [group_index % configuration.theme.colors.groups.len()];
502 let is_line = component_coords.len() == 2;
503
504 let coords = if component_coords.len() == 1 {
505 let (x, y) = component_coords[0];
507 let radius = 3.0;
508 let segments = 16;
509 let mut circle_coords = Vec::new();
510 for i in 0..=segments {
511 let angle = (f64::from(i) / f64::from(segments)) * 2.0 * PI;
512 circle_coords.push((x + radius * angle.cos(), y + radius * angle.sin()));
513 }
514 circle_coords
515 } else if is_line {
516 component_coords
518 } else {
519 let points: Vec<Point> = component_coords
521 .iter()
522 .map(|&(x, y)| Point::new(x, y))
523 .collect();
524
525 let line_string: LineString = points.into();
526 let hull = line_string.concave_hull();
527 hull.exterior().coords().map(|c| (c.x, c.y)).collect()
528 };
529
530 if coords.is_empty() {
531 continue;
532 }
533
534 let (r, g, b) = *color;
536 offscreen_context.set_source_rgb(r, g, b);
537 offscreen_context.move_to(coords[0].0, coords[0].1);
538 for &(x, y) in &coords[1..] {
539 offscreen_context.line_to(x, y);
540 }
541
542 if !is_line {
544 offscreen_context.close_path();
545 offscreen_context.fill_preserve()?;
546 }
547
548 offscreen_context.set_line_width(configuration.theme.sizes.vertex_size * 1.75);
550 offscreen_context.set_line_cap(LineCap::Round);
551 offscreen_context.set_line_join(LineJoin::Round);
552 offscreen_context.stroke()?;
553 }
554
555 context.save()?;
557 context.set_source_surface(&offscreen, 0.0, 0.0)?;
558 context.paint_with_alpha(configuration.theme.opacity.groups)?;
559 context.restore()?;
560
561 Ok(())
562}
563
564fn draw_vertices(
565 context: &Context,
566 map: &Map,
567 configuration: &Configuration,
568) -> Result<(), RenderError> {
569 context.save()?;
570 let (red, green, blue) = configuration.theme.colors.vertex;
571 context.set_source_rgb(red, green, blue);
572
573 for component in &map.components {
574 let vertex_x =
575 percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
576 let vertex_y = percent_to_pixel(
577 component.coordinates.1,
578 configuration.theme.sizes.map_height,
579 );
580
581 draw_vertex(
582 context,
583 &component.shape,
584 vertex_x,
585 vertex_y,
586 configuration.theme.sizes.vertex_size,
587 configuration.theme.sizes.vertex_size,
588 )?;
589 }
590
591 context.restore()?;
592 Ok(())
593}
594
595fn draw_vertex_labels(
596 context: &Context,
597 map: &Map,
598 configuration: &Configuration,
599 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
600) -> Result<(), RenderError> {
601 context.save()?;
602 let (red, green, blue) = configuration.theme.colors.label;
603 context.set_source_rgb(red, green, blue);
604 context.select_font_face(
605 &configuration.theme.fonts.face,
606 FontSlant::Normal,
607 FontWeight::Normal,
608 );
609 context.set_font_size(configuration.theme.fonts.vertex_label);
610
611 if configuration.options.smart_label_positioning {
612 let has_significant_complexity = map.dependencies.len() + map.evolutions.len() > 10;
614
615 if has_significant_complexity {
616 let all_lines = collect_all_lines(
619 map,
620 configuration.theme.sizes.map_width,
621 configuration.theme.sizes.map_height,
622 configuration.theme.sizes.vertex_size,
623 component_map,
624 );
625
626 let spatial_grid = build_spatial_grid(
627 &all_lines,
628 configuration.theme.sizes.map_width,
629 configuration.theme.sizes.map_height,
630 );
631
632 let note_rects = collect_note_rects(
633 map,
634 configuration.theme.sizes.map_width,
635 configuration.theme.sizes.map_height,
636 context,
637 configuration.theme.fonts.vertex_label,
638 )?;
639
640 for component in &map.components {
641 let position = calculate_optimal_label_position_cached(
642 component,
643 &PositioningDimensions {
644 map_size: (
645 configuration.theme.sizes.map_width,
646 configuration.theme.sizes.map_height,
647 ),
648 vertex_size: configuration.theme.sizes.vertex_size,
649 font_size: configuration.theme.fonts.vertex_label,
650 },
651 context,
652 &all_lines,
653 &spatial_grid,
654 ¬e_rects,
655 )?;
656
657 let label_x = position.x;
658 let label_y = position.y + 7.0; context.move_to(label_x, label_y);
661 context.show_text(&component.label)?;
662 }
663 } else {
664 for component in &map.components {
666 let vertex_x =
667 percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
668 let vertex_y = percent_to_pixel(
669 component.coordinates.1,
670 configuration.theme.sizes.map_height,
671 );
672 let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0;
673 let label_y = vertex_y
674 + configuration.theme.sizes.vertex_size / 2.0
675 + configuration.theme.fonts.vertex_label / 3.0;
676
677 context.move_to(label_x, label_y);
678 context.show_text(&component.label)?;
679 }
680 }
681 } else {
682 for component in &map.components {
684 let vertex_x =
685 percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
686 let vertex_y = percent_to_pixel(
687 component.coordinates.1,
688 configuration.theme.sizes.map_height,
689 );
690 let label_x = vertex_x + configuration.theme.sizes.vertex_size + 5.0;
691 let label_y = vertex_y
692 + configuration.theme.sizes.vertex_size / 2.0
693 + configuration.theme.fonts.vertex_label / 3.0;
694
695 context.move_to(label_x, label_y);
696 context.show_text(&component.label)?;
697 }
698 }
699
700 context.restore()?;
701 Ok(())
702}
703
704fn draw_inertias(
705 context: &Context,
706 map: &Map,
707 configuration: &Configuration,
708 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
709) -> Result<(), RenderError> {
710 context.save()?;
711 let (red, green, blue) = configuration.theme.colors.inertia;
712 context.set_source_rgb(red, green, blue);
713
714 let corner_radius = 2.0;
715 let rect_width = configuration.theme.sizes.vertex_size / 2.0;
716 let rect_height = configuration.theme.sizes.vertex_size * 2.0;
717
718 for inertia in &map.inertias {
719 if let Some(&(origin_x, origin_y)) = component_map.get(&inertia.component.to_lowercase()) {
720 let inertia_x = origin_x + 3.0 * configuration.theme.sizes.vertex_size;
722 let inertia_y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0;
723
724 draw_rounded_rect(
726 context,
727 inertia_x,
728 inertia_y,
729 rect_width,
730 rect_height,
731 corner_radius,
732 );
733 context.fill()?;
734 }
735 }
736
737 context.restore()?;
738 Ok(())
739}
740
741fn draw_rounded_rect(context: &Context, x: f64, y: f64, width: f64, height: f64, radius: f64) {
742 let r = radius;
743 context.new_path();
744 context.arc(
745 x + r,
746 y + r,
747 r,
748 std::f64::consts::PI,
749 3.0 * std::f64::consts::PI / 2.0,
750 );
751 context.arc(
752 x + width - r,
753 y + r,
754 r,
755 3.0 * std::f64::consts::PI / 2.0,
756 0.0,
757 );
758 context.arc(
759 x + width - r,
760 y + height - r,
761 r,
762 0.0,
763 std::f64::consts::PI / 2.0,
764 );
765 context.arc(
766 x + r,
767 y + height - r,
768 r,
769 std::f64::consts::PI / 2.0,
770 std::f64::consts::PI,
771 );
772 context.close_path();
773}
774
775fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result<Vec<String>, Error> {
777 let words: Vec<&str> = text.split_whitespace().collect();
778 let mut lines = Vec::new();
779 let mut current_line = String::new();
780
781 for word in words {
782 let test_line = if current_line.is_empty() {
783 word.to_string()
784 } else {
785 format!("{current_line} {word}")
786 };
787
788 let extents = context.text_extents(&test_line)?;
789
790 if extents.width() <= max_width {
791 current_line = test_line;
792 } else {
793 if !current_line.is_empty() {
794 lines.push(current_line);
795 }
796 current_line = word.to_string();
797 }
798 }
799
800 if !current_line.is_empty() {
801 lines.push(current_line);
802 }
803
804 if lines.is_empty() {
806 lines.push(text.to_string());
807 }
808
809 Ok(lines)
810}
811
812fn draw_evolutions(
813 context: &Context,
814 map: &Map,
815 configuration: &Configuration,
816 component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
817) -> Result<(), RenderError> {
818 context.save()?;
819 let (r, g, b) = configuration.theme.colors.evolution;
820 context.set_source_rgb(r, g, b);
821 context.set_line_width(configuration.theme.sizes.line_width * 2.0);
822 context.set_dash(&[10.0], 0.0);
823
824 for evolution in &map.evolutions {
825 if let Some(&(origin_x, origin_y)) = component_map.get(&evolution.component.to_lowercase())
826 {
827 let evolution_offset =
829 percent_to_pixel(evolution.value, configuration.theme.sizes.map_width);
830 let dest_x = (origin_x + evolution_offset)
831 .max(0.0)
832 .min(configuration.theme.sizes.map_width);
833 let dest_y = origin_y;
834
835 let multiplier = if dest_x > origin_x { 1.0 } else { -1.0 };
837
838 let offset_origin_x = origin_x
840 + multiplier * (configuration.theme.sizes.vertex_size / 2.0)
841 + configuration.theme.sizes.vertex_size / 2.0;
842 let offset_origin_y = origin_y + configuration.theme.sizes.vertex_size / 2.0;
843 let offset_dest_x = dest_x - multiplier * (configuration.theme.sizes.vertex_size / 2.0)
844 + configuration.theme.sizes.vertex_size / 2.0;
845 let offset_dest_y = dest_y + configuration.theme.sizes.vertex_size / 2.0;
846
847 context.move_to(offset_origin_x, offset_origin_y);
849 context.line_to(offset_dest_x, offset_dest_y);
850
851 let upper_angle = -PI / 4.0;
853 let lower_angle = PI / 4.0;
854 let arrowhead_size = configuration.theme.sizes.arrowhead_size;
855
856 context.move_to(offset_dest_x, offset_dest_y);
857 context.line_to(
858 offset_dest_x - multiplier * arrowhead_size * upper_angle.cos(),
859 offset_dest_y - multiplier * arrowhead_size * upper_angle.sin(),
860 );
861
862 context.move_to(offset_dest_x, offset_dest_y);
863 context.line_to(
864 offset_dest_x - multiplier * arrowhead_size * lower_angle.cos(),
865 offset_dest_y - multiplier * arrowhead_size * lower_angle.sin(),
866 );
867 }
868 }
869
870 context.stroke()?;
872
873 context.restore()?;
874 Ok(())
875}
876
877fn draw_notes(
878 context: &Context,
879 map: &Map,
880 configuration: &Configuration,
881) -> Result<(), RenderError> {
882 let padding = 5.0;
883 let max_width = 400.0;
884
885 context.save()?;
886 context.select_font_face(
887 &configuration.theme.fonts.face,
888 FontSlant::Normal,
889 FontWeight::Normal,
890 );
891 context.set_font_size(configuration.theme.fonts.note);
892
893 for note in &map.notes {
894 let note_x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width);
895 let note_y = percent_to_pixel(note.coordinates.1, configuration.theme.sizes.map_height);
896
897 let text = note.text.replace("\\n", "\n");
899 let lines: Vec<&str> = text.split('\n').collect();
900
901 let mut widest_line = 0.0;
903 for line in &lines {
904 let extents = context.text_extents(line)?;
905 if extents.width() > widest_line {
906 widest_line = extents.width();
907 }
908 }
909
910 let box_width = f64::min(max_width, widest_line + padding * 2.0);
911 #[allow(clippy::cast_precision_loss)]
913 let box_height = lines.len() as f64 * configuration.theme.line_heights.note + padding * 2.0;
914
915 let (red, green, blue) = configuration.theme.colors.background;
917 context.set_source_rgb(red, green, blue);
918 context.rectangle(note_x, note_y, box_width, box_height);
919 context.fill()?;
920
921 let (red, green, blue) = configuration.theme.colors.vertex;
923 context.set_source_rgb(red, green, blue);
924 context.set_line_width(configuration.theme.sizes.line_width);
925 context.rectangle(note_x, note_y, box_width, box_height);
926 context.stroke()?;
927
928 let (red, green, blue) = configuration.theme.colors.label;
930 context.set_source_rgb(red, green, blue);
931 for (i, line) in lines.iter().enumerate() {
932 #[allow(clippy::cast_precision_loss)]
934 let line_offset = i as f64 * configuration.theme.line_heights.note;
935 context.move_to(
936 note_x + padding,
937 note_y + padding + line_offset + configuration.theme.fonts.note,
938 );
939 context.show_text(line)?;
940 }
941 }
942
943 context.restore()?;
944 Ok(())
945}
946
947fn get_stage_positions(map: &Map, configuration: &Configuration) -> [f64; 3] {
949 let default_stages = [25.0, 50.0, 75.0];
950 let mut stages = default_stages;
951
952 for stage_data in &map.stages {
954 let index = match stage_data.stage {
955 Stage::I => 0,
956 Stage::Ii => 1,
957 Stage::Iii => 2,
958 Stage::Iv => continue, };
960 stages[index] = stage_data.value; }
962
963 [
964 percent_to_pixel(stages[0], configuration.theme.sizes.map_width),
965 percent_to_pixel(stages[1], configuration.theme.sizes.map_width),
966 percent_to_pixel(stages[2], configuration.theme.sizes.map_width),
967 ]
968}