1use crate::core::{
9 scale_path, scale_paths, transform_paths, Path64, PathD, Paths64, PathsD, PointD, RectD,
10};
11use crate::FillRule;
12use std::fs;
13use std::io::Write;
14
15pub const SUBJ_BRUSH_CLR: u32 = 0x1800009C;
21pub const SUBJ_STROKE_CLR: u32 = 0xFFB3B3DA;
22pub const CLIP_BRUSH_CLR: u32 = 0x129C0000;
23pub const CLIP_STROKE_CLR: u32 = 0xCCFFA07A;
24pub const SOLUTION_BRUSH_CLR: u32 = 0x4466FF66;
25
26pub fn color_to_html(clr: u32) -> String {
33 format!("#{:06x}", clr & 0xFFFFFF)
34}
35
36pub fn get_alpha_as_frac(clr: u32) -> f32 {
39 (clr >> 24) as f32 / 255.0
40}
41
42#[derive(Debug, Clone)]
49pub struct PathInfo {
50 pub paths: PathsD,
51 pub is_open_path: bool,
52 pub fillrule: FillRule,
53 pub brush_color: u32,
54 pub pen_color: u32,
55 pub pen_width: f64,
56 pub show_coords: bool,
57}
58
59impl PathInfo {
60 pub fn new(
61 paths: PathsD,
62 is_open: bool,
63 fillrule: FillRule,
64 brush_color: u32,
65 pen_color: u32,
66 pen_width: f64,
67 show_coords: bool,
68 ) -> Self {
69 Self {
70 paths,
71 is_open_path: is_open,
72 fillrule,
73 brush_color,
74 pen_color,
75 pen_width,
76 show_coords,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
88pub struct TextInfo {
89 pub text: String,
90 pub font_name: String,
91 pub font_color: u32,
92 pub font_weight: u32,
93 pub font_size: u32,
94 pub x: f64,
95 pub y: f64,
96}
97
98impl TextInfo {
99 pub fn new(
100 text: &str,
101 font_name: &str,
102 font_color: u32,
103 font_weight: u32,
104 font_size: u32,
105 x: f64,
106 y: f64,
107 ) -> Self {
108 Self {
109 text: text.to_string(),
110 font_name: font_name.to_string(),
111 font_color,
112 font_weight,
113 font_size,
114 x,
115 y,
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
125struct CoordsStyle {
126 font_name: String,
127 font_color: u32,
128 font_size: u32,
129}
130
131impl Default for CoordsStyle {
132 fn default() -> Self {
133 Self {
134 font_name: "Verdana".to_string(),
135 font_color: 0xFF000000,
136 font_size: 11,
137 }
138 }
139}
140
141pub struct SvgWriter {
161 scale: f64,
162 fill_rule: FillRule,
163 coords_style: CoordsStyle,
164 text_infos: Vec<TextInfo>,
165 path_infos: Vec<PathInfo>,
166}
167
168impl SvgWriter {
169 pub fn new(precision: i32) -> Self {
172 Self {
173 scale: 10.0_f64.powi(precision),
174 fill_rule: FillRule::NonZero,
175 coords_style: CoordsStyle::default(),
176 text_infos: Vec::new(),
177 path_infos: Vec::new(),
178 }
179 }
180
181 pub fn clear(&mut self) {
183 self.path_infos.clear();
184 self.text_infos.clear();
185 }
186
187 pub fn fill_rule(&self) -> FillRule {
189 self.fill_rule
190 }
191
192 pub fn set_coords_style(&mut self, font_name: &str, font_color: u32, font_size: u32) {
195 self.coords_style.font_name = font_name.to_string();
196 self.coords_style.font_color = font_color;
197 self.coords_style.font_size = font_size;
198 }
199
200 pub fn add_text(&mut self, text: &str, font_color: u32, font_size: u32, x: f64, y: f64) {
203 self.text_infos
204 .push(TextInfo::new(text, "", font_color, 600, font_size, x, y));
205 }
206
207 #[allow(clippy::too_many_arguments)]
210 pub fn add_path_64(
211 &mut self,
212 path: &Path64,
213 is_open: bool,
214 fillrule: FillRule,
215 brush_color: u32,
216 pen_color: u32,
217 pen_width: f64,
218 show_coords: bool,
219 ) {
220 if path.is_empty() {
221 return;
222 }
223 let mut error_code = 0;
224 let scaled: PathD = scale_path::<f64, i64>(path, self.scale, self.scale, &mut error_code);
225 if error_code != 0 {
226 return;
227 }
228 self.path_infos.push(PathInfo::new(
229 vec![scaled],
230 is_open,
231 fillrule,
232 brush_color,
233 pen_color,
234 pen_width,
235 show_coords,
236 ));
237 }
238
239 #[allow(clippy::too_many_arguments)]
242 pub fn add_path_d(
243 &mut self,
244 path: &PathD,
245 is_open: bool,
246 fillrule: FillRule,
247 brush_color: u32,
248 pen_color: u32,
249 pen_width: f64,
250 show_coords: bool,
251 ) {
252 if path.is_empty() {
253 return;
254 }
255 self.path_infos.push(PathInfo::new(
256 vec![path.clone()],
257 is_open,
258 fillrule,
259 brush_color,
260 pen_color,
261 pen_width,
262 show_coords,
263 ));
264 }
265
266 #[allow(clippy::too_many_arguments)]
269 pub fn add_paths_64(
270 &mut self,
271 paths: &Paths64,
272 is_open: bool,
273 fillrule: FillRule,
274 brush_color: u32,
275 pen_color: u32,
276 pen_width: f64,
277 show_coords: bool,
278 ) {
279 if paths.is_empty() {
280 return;
281 }
282 let mut error_code = 0;
283 let scaled: PathsD =
284 scale_paths::<f64, i64>(paths, self.scale, self.scale, &mut error_code);
285 if error_code != 0 {
286 return;
287 }
288 self.path_infos.push(PathInfo::new(
289 scaled,
290 is_open,
291 fillrule,
292 brush_color,
293 pen_color,
294 pen_width,
295 show_coords,
296 ));
297 }
298
299 #[allow(clippy::too_many_arguments)]
302 pub fn add_paths_d(
303 &mut self,
304 paths: &PathsD,
305 is_open: bool,
306 fillrule: FillRule,
307 brush_color: u32,
308 pen_color: u32,
309 pen_width: f64,
310 show_coords: bool,
311 ) {
312 if paths.is_empty() {
313 return;
314 }
315 self.path_infos.push(PathInfo::new(
316 paths.clone(),
317 is_open,
318 fillrule,
319 brush_color,
320 pen_color,
321 pen_width,
322 show_coords,
323 ));
324 }
325
326 pub fn save_to_file(
331 &self,
332 filename: &str,
333 max_width: i32,
334 max_height: i32,
335 margin: i32,
336 ) -> bool {
337 let mut rec = RectD {
339 left: f64::MAX,
340 top: f64::MAX,
341 right: f64::MIN,
342 bottom: f64::MIN,
343 };
344 for pi in &self.path_infos {
345 for path in &pi.paths {
346 for pt in path {
347 if pt.x < rec.left {
348 rec.left = pt.x;
349 }
350 if pt.x > rec.right {
351 rec.right = pt.x;
352 }
353 if pt.y < rec.top {
354 rec.top = pt.y;
355 }
356 if pt.y > rec.bottom {
357 rec.bottom = pt.y;
358 }
359 }
360 }
361 }
362 if rec.left >= rec.right || rec.top >= rec.bottom {
363 return false;
364 }
365
366 let margin = margin.max(20);
367 let max_width = max_width.max(100);
368 let max_height = max_height.max(100);
369
370 let rec_width = rec.right - rec.left;
371 let rec_height = rec.bottom - rec.top;
372 let scale = ((max_width - margin * 2) as f64 / rec_width)
373 .min((max_height - margin * 2) as f64 / rec_height);
374
375 rec.left *= scale;
376 rec.top *= scale;
377 rec.right *= scale;
378 rec.bottom *= scale;
379 let offset_x = margin as f64 - rec.left;
380 let offset_y = margin as f64 - rec.top;
381
382 let file = fs::File::create(filename);
383 let mut file = match file {
384 Ok(f) => f,
385 Err(_) => return false,
386 };
387
388 let header = format!(
390 "<?xml version=\"1.0\" standalone=\"no\"?>\n\
391 <!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n\
392 \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n\
393 <svg width=\"{}px\" height=\"{}px\" viewBox=\"0 0 {} {}\" \
394 version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n\n",
395 max_width, max_height, max_width, max_height
396 );
397 if write!(file, "{}", header).is_err() {
398 return false;
399 }
400
401 for pi in &self.path_infos {
408 let brush_color = pi.brush_color;
409
410 let _ = write!(file, " <path d=\"");
411 for path in &pi.paths {
412 if path.len() < 2 || (path.len() == 2 && !pi.is_open_path) {
413 continue;
414 }
415 let _ = write!(
416 file,
417 " M {:.2} {:.2}",
418 path[0].x * scale + offset_x,
419 path[0].y * scale + offset_y
420 );
421 for pt in path {
422 let _ = write!(
423 file,
424 " L {:.2} {:.2}",
425 pt.x * scale + offset_x,
426 pt.y * scale + offset_y
427 );
428 }
429 if !pi.is_open_path {
430 let _ = write!(file, " z");
431 }
432 }
433
434 let fill_rule_str = if pi.fillrule == FillRule::NonZero {
435 "nonzero"
436 } else {
437 "evenodd"
438 };
439
440 let _ = write!(
441 file,
442 "\"\n style=\"fill:{}; fill-opacity:{:.2}; fill-rule:{}; \
443 stroke:{}; stroke-opacity:{:.2}; stroke-width:{:.1};\"/>\n",
444 color_to_html(brush_color),
445 get_alpha_as_frac(brush_color),
446 fill_rule_str,
447 color_to_html(pi.pen_color),
448 get_alpha_as_frac(pi.pen_color),
449 pi.pen_width
450 );
451
452 if pi.show_coords {
454 let _ = writeln!(
455 file,
456 " <g font-family=\"{}\" font-size=\"{}\" fill=\"{}\" fill-opacity=\"{:.2}\">",
457 self.coords_style.font_name,
458 self.coords_style.font_size,
459 color_to_html(self.coords_style.font_color),
460 get_alpha_as_frac(self.coords_style.font_color)
461 );
462 for path in &pi.paths {
463 if path.len() < 2 || (path.len() == 2 && !pi.is_open_path) {
464 continue;
465 }
466 for pt in path {
467 let _ = writeln!(
468 file,
469 " <text x=\"{}\" y=\"{}\">{:.0},{:.0}</text>",
470 (pt.x * scale + offset_x) as i64,
471 (pt.y * scale + offset_y) as i64,
472 pt.x,
473 pt.y
474 );
475 }
476 }
477 let _ = writeln!(file, " </g>\n");
478 }
479 }
480
481 for ti in &self.text_infos {
483 let _ = writeln!(
484 file,
485 " <g font-family=\"{}\" font-size=\"{}\" fill=\"{}\" fill-opacity=\"{:.2}\">",
486 if ti.font_name.is_empty() {
487 "Verdana"
488 } else {
489 &ti.font_name
490 },
491 ti.font_size,
492 color_to_html(ti.font_color),
493 get_alpha_as_frac(ti.font_color)
494 );
495 let _ = writeln!(
496 file,
497 " <text x=\"{}\" y=\"{}\">{}</text>\n </g>\n",
498 (ti.x * scale + offset_x) as i64,
499 (ti.y * scale + offset_y) as i64,
500 ti.text
501 );
502 }
503
504 let _ = writeln!(file, "</svg>");
505 true
506 }
507}
508
509pub struct SvgReader {
518 pub xml: String,
519 path_infos: Vec<PathInfo>,
520}
521
522impl SvgReader {
523 pub fn new() -> Self {
524 Self {
525 xml: String::new(),
526 path_infos: Vec::new(),
527 }
528 }
529
530 pub fn clear(&mut self) {
531 self.path_infos.clear();
532 }
533
534 pub fn load_from_file(&mut self, filename: &str) -> bool {
537 self.clear();
538 let content = match fs::read_to_string(filename) {
539 Ok(s) => s,
540 Err(_) => return false,
541 };
542 self.xml = content;
543 self.parse_paths();
544 !self.path_infos.is_empty()
545 }
546
547 fn parse_paths(&mut self) {
549 let xml = self.xml.clone();
550 let mut pos = 0;
551 while let Some(path_start) = xml[pos..].find("<path") {
552 let abs_start = pos + path_start + 5;
553 if let Some(path_end) = xml[abs_start..].find("/>") {
554 let abs_end = abs_start + path_end;
555 let element = &xml[abs_start..abs_end];
556 self.load_path(element);
557 pos = abs_end + 2;
558 } else {
559 break;
560 }
561 }
562 }
563
564 fn load_path(&mut self, element: &str) -> bool {
567 let d_attr = match element.find("d=\"") {
568 Some(pos) => &element[pos + 3..],
569 None => return false,
570 };
571
572 let d_end = match d_attr.find('"') {
573 Some(pos) => pos,
574 None => return false,
575 };
576 let d_value = &d_attr[..d_end];
577
578 let mut paths: PathsD = Vec::new();
579 let mut current_path: PathD = Vec::new();
580 let mut x: f64;
581 let mut y: f64;
582 let mut command;
583 let mut is_relative;
584
585 let chars: Vec<char> = d_value.chars().collect();
586 let mut i = 0;
587
588 while i < chars.len() && chars[i].is_whitespace() {
590 i += 1;
591 }
592
593 if i >= chars.len() {
595 return false;
596 }
597 if chars[i] == 'M' {
598 is_relative = false;
599 i += 1;
600 } else if chars[i] == 'm' {
601 is_relative = true;
602 i += 1;
603 } else {
604 return false;
605 }
606 command = 'M';
607
608 if let Some((val, next)) = parse_number(&chars, i) {
610 x = val;
611 i = next;
612 } else {
613 return false;
614 }
615 if let Some((val, next)) = parse_number(&chars, i) {
616 y = val;
617 i = next;
618 } else {
619 return false;
620 }
621 current_path.push(PointD::new(x, y));
622
623 while i < chars.len() {
625 while i < chars.len() && chars[i].is_whitespace() {
627 i += 1;
628 }
629 if i >= chars.len() {
630 break;
631 }
632
633 if chars[i].is_ascii_alphabetic() {
635 let ch = chars[i];
636 match ch.to_ascii_uppercase() {
637 'L' | 'M' => {
638 command = ch.to_ascii_uppercase();
639 is_relative = ch.is_ascii_lowercase();
640 i += 1;
641 }
642 'H' => {
643 command = 'H';
644 is_relative = ch.is_ascii_lowercase();
645 i += 1;
646 }
647 'V' => {
648 command = 'V';
649 is_relative = ch.is_ascii_lowercase();
650 i += 1;
651 }
652 'Z' => {
653 if current_path.len() > 2 {
654 paths.push(current_path.clone());
655 }
656 current_path.clear();
657 i += 1;
658 continue;
659 }
660 _ => break, }
662 }
663
664 match command {
666 'H' => {
667 if let Some((val, next)) = parse_number(&chars, i) {
668 x = if is_relative { x + val } else { val };
669 current_path.push(PointD::new(x, y));
670 i = next;
671 } else {
672 break;
673 }
674 }
675 'V' => {
676 if let Some((val, next)) = parse_number(&chars, i) {
677 y = if is_relative { y + val } else { val };
678 current_path.push(PointD::new(x, y));
679 i = next;
680 } else {
681 break;
682 }
683 }
684 'L' | 'M' => {
685 if let Some((vx, next1)) = parse_number(&chars, i) {
686 if let Some((vy, next2)) = parse_number(&chars, next1) {
687 x = if is_relative { x + vx } else { vx };
688 y = if is_relative { y + vy } else { vy };
689 current_path.push(PointD::new(x, y));
690 i = next2;
691 } else {
692 break;
693 }
694 } else {
695 break;
696 }
697 }
698 _ => break,
699 }
700 }
701
702 if current_path.len() > 3 {
704 paths.push(current_path);
705 }
706
707 if paths.is_empty() {
708 return false;
709 }
710
711 self.path_infos.push(PathInfo::new(
712 paths,
713 false,
714 FillRule::EvenOdd,
715 0,
716 0xFF000000,
717 1.0,
718 false,
719 ));
720 true
721 }
722
723 pub fn get_paths(&self) -> PathsD {
726 let mut result = PathsD::new();
727 for pi in &self.path_infos {
728 for path in &pi.paths {
729 result.push(path.clone());
730 }
731 }
732 result
733 }
734}
735
736impl Default for SvgReader {
737 fn default() -> Self {
738 Self::new()
739 }
740}
741
742pub fn svg_add_caption(svg: &mut SvgWriter, caption: &str, x: i32, y: i32) {
748 svg.add_text(caption, 0xFF000000, 14, x as f64, y as f64);
749}
750
751pub fn svg_add_subject_64(svg: &mut SvgWriter, paths: &Paths64, fillrule: FillRule) {
753 let tmp: PathsD = transform_paths(paths);
754 svg.add_paths_d(
755 &tmp,
756 false,
757 fillrule,
758 SUBJ_BRUSH_CLR,
759 SUBJ_STROKE_CLR,
760 0.8,
761 false,
762 );
763}
764
765pub fn svg_add_subject_d(svg: &mut SvgWriter, paths: &PathsD, fillrule: FillRule) {
767 svg.add_paths_d(
768 paths,
769 false,
770 fillrule,
771 SUBJ_BRUSH_CLR,
772 SUBJ_STROKE_CLR,
773 0.8,
774 false,
775 );
776}
777
778pub fn svg_add_open_subject_64(svg: &mut SvgWriter, paths: &Paths64, fillrule: FillRule) {
780 let tmp: PathsD = transform_paths(paths);
781 svg.add_paths_d(&tmp, true, fillrule, 0x0, 0xCCB3B3DA, 1.3, false);
782}
783
784pub fn svg_add_open_subject_d(
786 svg: &mut SvgWriter,
787 paths: &PathsD,
788 fillrule: FillRule,
789 is_joined: bool,
790) {
791 if is_joined {
792 svg.add_paths_d(
793 paths,
794 false,
795 fillrule,
796 SUBJ_BRUSH_CLR,
797 SUBJ_STROKE_CLR,
798 1.3,
799 false,
800 );
801 } else {
802 svg.add_paths_d(paths, true, fillrule, 0x0, SUBJ_STROKE_CLR, 1.3, false);
803 }
804}
805
806pub fn svg_add_clip_64(svg: &mut SvgWriter, paths: &Paths64, fillrule: FillRule) {
808 let tmp: PathsD = transform_paths(paths);
809 svg.add_paths_d(
810 &tmp,
811 false,
812 fillrule,
813 CLIP_BRUSH_CLR,
814 CLIP_STROKE_CLR,
815 0.8,
816 false,
817 );
818}
819
820pub fn svg_add_clip_d(svg: &mut SvgWriter, paths: &PathsD, fillrule: FillRule) {
822 svg.add_paths_d(
823 paths,
824 false,
825 fillrule,
826 CLIP_BRUSH_CLR,
827 CLIP_STROKE_CLR,
828 0.8,
829 false,
830 );
831}
832
833pub fn svg_add_solution_64(
835 svg: &mut SvgWriter,
836 paths: &Paths64,
837 fillrule: FillRule,
838 show_coords: bool,
839) {
840 let tmp: PathsD = transform_paths(paths);
841 svg.add_paths_d(
842 &tmp,
843 false,
844 fillrule,
845 SOLUTION_BRUSH_CLR,
846 0xFF003300,
847 1.0,
848 show_coords,
849 );
850}
851
852pub fn svg_add_solution_d(
854 svg: &mut SvgWriter,
855 paths: &PathsD,
856 fillrule: FillRule,
857 show_coords: bool,
858) {
859 svg.add_paths_d(
860 paths,
861 false,
862 fillrule,
863 SOLUTION_BRUSH_CLR,
864 0xFF003300,
865 1.2,
866 show_coords,
867 );
868}
869
870pub fn svg_add_open_solution_64(
872 svg: &mut SvgWriter,
873 paths: &Paths64,
874 fillrule: FillRule,
875 show_coords: bool,
876 is_joined: bool,
877) {
878 let tmp: PathsD = transform_paths(paths);
879 svg.add_paths_d(
880 &tmp,
881 !is_joined,
882 fillrule,
883 0x0,
884 0xFF006600,
885 1.8,
886 show_coords,
887 );
888}
889
890pub fn svg_add_open_solution_d(
892 svg: &mut SvgWriter,
893 paths: &PathsD,
894 fillrule: FillRule,
895 show_coords: bool,
896 is_joined: bool,
897) {
898 svg.add_paths_d(
899 paths,
900 !is_joined,
901 fillrule,
902 0x0,
903 0xFF006600,
904 1.8,
905 show_coords,
906 );
907}
908
909pub fn svg_save_to_file(
911 svg: &mut SvgWriter,
912 filename: &str,
913 max_width: i32,
914 max_height: i32,
915 margin: i32,
916) {
917 svg.set_coords_style("Verdana", 0xFF0000AA, 7);
918 svg.save_to_file(filename, max_width, max_height, margin);
919}
920
921fn parse_number(chars: &[char], start: usize) -> Option<(f64, usize)> {
928 let mut i = start;
929 while i < chars.len() && (chars[i].is_whitespace() || chars[i] == ',') {
931 i += 1;
932 }
933 if i >= chars.len() {
934 return None;
935 }
936
937 let start_pos = i;
938 let is_neg = chars[i] == '-';
939 if is_neg {
940 i += 1;
941 }
942 if chars.get(i) == Some(&'+') {
943 i += 1;
944 }
945
946 let mut has_digits = false;
947 let mut has_dot = false;
948
949 while i < chars.len() {
950 if chars[i] == '.' {
951 if has_dot {
952 break;
953 }
954 has_dot = true;
955 } else if chars[i].is_ascii_digit() {
956 has_digits = true;
957 } else {
958 break;
959 }
960 i += 1;
961 }
962
963 if !has_digits {
964 return None;
965 }
966
967 let num_str: String = chars[start_pos..i].iter().collect();
968 match num_str.parse::<f64>() {
969 Ok(val) => Some((val, i)),
970 Err(_) => None,
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977 use crate::core::Point64;
978
979 #[test]
980 fn test_color_to_html() {
981 assert_eq!(color_to_html(0xFF123456), "#123456");
982 assert_eq!(color_to_html(0x00000000), "#000000");
983 assert_eq!(color_to_html(0xFFFFFFFF), "#ffffff");
984 }
985
986 #[test]
987 fn test_get_alpha_as_frac() {
988 assert!((get_alpha_as_frac(0xFF000000) - 1.0).abs() < 0.01);
989 assert!((get_alpha_as_frac(0x80000000) - 0.502).abs() < 0.01);
990 assert!((get_alpha_as_frac(0x00000000) - 0.0).abs() < 0.01);
991 }
992
993 #[test]
994 fn test_svg_writer_new() {
995 let svg = SvgWriter::new(0);
996 assert_eq!(svg.fill_rule(), FillRule::NonZero);
997 }
998
999 #[test]
1000 fn test_svg_writer_add_text() {
1001 let mut svg = SvgWriter::new(0);
1002 svg.add_text("Hello", 0xFF000000, 12, 10.0, 20.0);
1003 assert_eq!(svg.text_infos.len(), 1);
1004 assert_eq!(svg.text_infos[0].text, "Hello");
1005 }
1006
1007 #[test]
1008 fn test_svg_writer_add_paths() {
1009 let mut svg = SvgWriter::new(0);
1010 let paths = vec![vec![
1011 Point64::new(0, 0),
1012 Point64::new(100, 0),
1013 Point64::new(100, 100),
1014 Point64::new(0, 100),
1015 ]];
1016 svg.add_paths_64(
1017 &paths,
1018 false,
1019 FillRule::NonZero,
1020 0xFF0000FF,
1021 0xFF000000,
1022 1.0,
1023 false,
1024 );
1025 assert_eq!(svg.path_infos.len(), 1);
1026 }
1027
1028 #[test]
1029 fn test_svg_writer_add_empty_paths() {
1030 let mut svg = SvgWriter::new(0);
1031 let paths: Paths64 = vec![];
1032 svg.add_paths_64(&paths, false, FillRule::NonZero, 0, 0, 1.0, false);
1033 assert_eq!(svg.path_infos.len(), 0);
1034 }
1035
1036 #[test]
1037 fn test_svg_writer_clear() {
1038 let mut svg = SvgWriter::new(0);
1039 svg.add_text("Test", 0xFF000000, 12, 0.0, 0.0);
1040 let paths = vec![vec![
1041 PointD::new(0.0, 0.0),
1042 PointD::new(1.0, 1.0),
1043 PointD::new(2.0, 0.0),
1044 ]];
1045 svg.add_paths_d(&paths, false, FillRule::NonZero, 0, 0, 1.0, false);
1046 svg.clear();
1047 assert!(svg.path_infos.is_empty());
1048 assert!(svg.text_infos.is_empty());
1049 }
1050
1051 #[test]
1052 fn test_svg_writer_save_to_file() {
1053 let mut svg = SvgWriter::new(0);
1054 let paths = vec![vec![
1055 PointD::new(0.0, 0.0),
1056 PointD::new(100.0, 0.0),
1057 PointD::new(100.0, 100.0),
1058 PointD::new(0.0, 100.0),
1059 ]];
1060 svg.add_paths_d(
1061 &paths,
1062 false,
1063 FillRule::NonZero,
1064 0x800000FF,
1065 0xFF000000,
1066 1.0,
1067 false,
1068 );
1069 svg.add_text("Test SVG", 0xFF000000, 14, 10.0, 10.0);
1070
1071 let tmp_file = std::env::temp_dir().join("clipper2_test_output.svg");
1072 let result = svg.save_to_file(tmp_file.to_str().unwrap(), 800, 600, 20);
1073 assert!(result);
1074
1075 let content = fs::read_to_string(&tmp_file).unwrap();
1076 assert!(content.contains("<svg"));
1077 assert!(content.contains("</svg>"));
1078 assert!(content.contains("<path"));
1079 assert!(content.contains("Test SVG"));
1080
1081 let _ = fs::remove_file(&tmp_file);
1082 }
1083
1084 #[test]
1085 fn test_svg_reader_new() {
1086 let reader = SvgReader::new();
1087 assert!(reader.xml.is_empty());
1088 assert!(reader.get_paths().is_empty());
1089 }
1090
1091 #[test]
1092 fn test_svg_reader_roundtrip() {
1093 let mut writer = SvgWriter::new(0);
1095 let paths = vec![vec![
1096 PointD::new(10.0, 10.0),
1097 PointD::new(90.0, 10.0),
1098 PointD::new(90.0, 90.0),
1099 PointD::new(10.0, 90.0),
1100 ]];
1101 writer.add_paths_d(
1102 &paths,
1103 false,
1104 FillRule::NonZero,
1105 0x800000FF,
1106 0xFF000000,
1107 1.0,
1108 false,
1109 );
1110
1111 let tmp_file = std::env::temp_dir().join("clipper2_test_roundtrip.svg");
1112 let filename = tmp_file.to_str().unwrap();
1113 assert!(writer.save_to_file(filename, 400, 400, 20));
1114
1115 let mut reader = SvgReader::new();
1116 assert!(reader.load_from_file(filename));
1117 let read_paths = reader.get_paths();
1118 assert!(!read_paths.is_empty());
1119
1120 let _ = fs::remove_file(&tmp_file);
1121 }
1122
1123 #[test]
1124 fn test_parse_number() {
1125 let chars: Vec<char> = "123.45, -67.8".chars().collect();
1126 let (val, next) = parse_number(&chars, 0).unwrap();
1127 assert!((val - 123.45).abs() < 0.001);
1128 let (val2, _) = parse_number(&chars, next).unwrap();
1129 assert!((val2 - (-67.8)).abs() < 0.001);
1130 }
1131
1132 #[test]
1133 fn test_parse_number_whitespace() {
1134 let chars: Vec<char> = " 42 ".chars().collect();
1135 let (val, _) = parse_number(&chars, 0).unwrap();
1136 assert!((val - 42.0).abs() < 0.001);
1137 }
1138
1139 #[test]
1140 fn test_parse_number_empty() {
1141 let chars: Vec<char> = " ".chars().collect();
1142 assert!(parse_number(&chars, 0).is_none());
1143 }
1144}