1use svg::node::element::path::Data;
2use svg::node::element::Path;
3use svg::{Document, Node};
4use url::Url;
5
6const SIDE_WING_SLOT_FROM_FRONT: usize = 20;
8const SIDE_WING_SLOT_WIDTH: usize = 20;
9const SIDE_WING_SLOT_SPACING: usize = 15;
10const CLEARANCE_BETWEEN_PATHS: usize = 3;
11const SIDE_TAP_FROM_FRONT: usize = 30;
12const SIDE_TAP_WIDTH: usize = 30;
13const CLEARANCE_FOR_CONTAINER_WIDTH: usize = 4;
14
15#[derive(Debug, Clone)]
16pub struct Container {
17 pub vendor: String,
18 pub model: String,
19 pub description: String,
20 pub links: Vec<ContainerLink>,
21 pub dimensions: ContainerDimensions,
22}
23
24impl Container {
25 pub fn key(&self) -> String {
26 format!("{}-{}", self.vendor, self.model)
27 .to_lowercase()
28 .replace(" ", "_")
29 }
30}
31
32#[derive(Debug, Clone)]
33pub struct ContainerDimensions {
34 pub width: usize,
35 pub depth: usize,
36 pub height: usize,
37 pub side_wing_from_box_top: usize,
38 pub side_wing_width: usize,
39}
40
41#[derive(Debug, Clone)]
42pub struct ContainerLink {
43 pub url: Url,
44 pub title: String,
45}
46#[derive(Debug, Clone)]
47pub struct AssembledDimensions {
48 pub width: f32,
49 pub height: f32,
50 pub depth: f32
51}
52#[derive(Debug, Clone)]
53pub struct GeneratedSvg {
54 pub document: Document,
55 pub assembled_dimensions: AssembledDimensions,
56}
57pub fn generate_svg(
58 rows: usize,
59 columns: usize,
60 material_thickness: f32,
61 container: &Container,
62 primary_color: &str,
63 secondary_color: &str,
64) -> GeneratedSvg {
65 let starting_point_x = 0.0;
66 let starting_point_y = 0.0;
67 let column_width = container.dimensions.width + CLEARANCE_FOR_CONTAINER_WIDTH;
68 let amount_of_boxes = (rows * columns) as usize;
69 let height_of_two_side_wings =
70 height_of_two_side_wings(container.dimensions.side_wing_width, material_thickness);
71 let height_of_two_side_wings_with_clearance =
72 height_of_two_side_wings + CLEARANCE_BETWEEN_PATHS as f32;
73
74 let total_width = (container.dimensions.depth + (CLEARANCE_BETWEEN_PATHS * 3)) as f32
75 + top_width(column_width as f32, columns, material_thickness)
76 + (container.dimensions.height * rows) as f32
77 + (2.0 * material_thickness);
78 let total_height = vec![
79 amount_of_boxes as f32 * height_of_two_side_wings_with_clearance,
80 (2 * container.dimensions.depth + CLEARANCE_BETWEEN_PATHS) as f32,
81 ((columns + 1) * (container.dimensions.depth + CLEARANCE_BETWEEN_PATHS)) as f32,
82 ]
83 .iter()
84 .cloned()
85 .fold(f32::NEG_INFINITY, f32::max);
86
87 let mut document = Document::new()
88 .set("viewBox", (0, 0, total_width, total_height))
89 .set("width", format!("{}mm", total_width))
90 .set("height", format!("{}mm", total_height));
91
92 for i in 0..amount_of_boxes {
94 generate_side_wing_pair(
95 &mut document,
96 &container.dimensions,
97 starting_point_x,
98 starting_point_y + height_of_two_side_wings_with_clearance * i as f32,
99 material_thickness,
100 secondary_color,
101 );
102 }
103
104 generate_top_and_bottom_pieces(
106 &mut document,
107 &container.dimensions,
108 (container.dimensions.depth + CLEARANCE_BETWEEN_PATHS) as f32,
109 columns,
110 column_width as f32 + material_thickness,
111 material_thickness,
112 primary_color,
113 secondary_color,
114 );
115
116 generate_side_panels(
118 &mut document,
119 (container.dimensions.depth + CLEARANCE_BETWEEN_PATHS) as f32 + top_width(column_width as f32, columns, material_thickness) + CLEARANCE_BETWEEN_PATHS as f32,
121 &container.dimensions, rows,
123 columns,
124 material_thickness,
125 primary_color,
126 secondary_color,
127 );
128
129 let assembled_width = (column_width * columns) as f32
131 + (columns + 1) as f32 * material_thickness;
132
133 let assembled_height = (container.dimensions.height * rows) as f32
134 + material_thickness * 2.0;
135
136 let assembled_depth = container.dimensions.depth as f32;
137
138 GeneratedSvg {
139 document,
140 assembled_dimensions: AssembledDimensions {
141 width: assembled_width,
142 height: assembled_height,
143 depth: assembled_depth
144 }
145 }
146}
147
148fn generate_side_panels(
149 document: &mut Document,
150 starting_point_x: f32,
151 dimensions: &ContainerDimensions,
152 rows: usize,
153 columns: usize,
154 material_thickness: f32,
155 primary_color: &str,
156 secondary_color: &str,
157) {
158 for i in 0..columns + 1 {
159 let y = (i * (dimensions.depth + CLEARANCE_BETWEEN_PATHS)) as f32;
160
161 document.append(generate_side_panel_outline_path(
162 starting_point_x,
163 y,
164 dimensions,
165 rows,
166 material_thickness,
167 secondary_color,
168 ));
169
170 for r in 0..rows {
171 let row_x = material_thickness
172 + (dimensions.side_wing_from_box_top + r * dimensions.height) as f32;
173
174 document.append(generate_side_panel_wing_holes(
175 starting_point_x + row_x,
176 y + SIDE_WING_SLOT_FROM_FRONT as f32,
177 material_thickness,
178 primary_color,
179 ));
180
181 document.append(generate_side_panel_wing_holes(
182 starting_point_x + row_x,
183 y + (SIDE_WING_SLOT_FROM_FRONT + SIDE_WING_SLOT_WIDTH + SIDE_WING_SLOT_SPACING)
184 as f32,
185 material_thickness,
186 primary_color,
187 ));
188
189 document.append(generate_side_panel_wing_holes(
190 starting_point_x + row_x,
191 y + (dimensions.depth
192 - SIDE_WING_SLOT_FROM_FRONT
193 - (2 * SIDE_WING_SLOT_WIDTH)
194 - SIDE_WING_SLOT_SPACING) as f32,
195 material_thickness,
196 primary_color,
197 ));
198 document.append(generate_side_panel_wing_holes(
199 starting_point_x + row_x,
200 y + (dimensions.depth - SIDE_WING_SLOT_FROM_FRONT - SIDE_WING_SLOT_WIDTH) as f32,
201 material_thickness,
202 primary_color,
203 ));
204 }
205 }
206}
207
208fn generate_side_panel_wing_holes(x: f32, y: f32, material_thickness: f32, color: &str) -> Path {
209 let path_data = Data::new()
210 .move_to((x, y))
211 .vertical_line_to(y + SIDE_WING_SLOT_WIDTH as f32)
212 .horizontal_line_to(x + material_thickness)
213 .vertical_line_to(y)
214 .close();
215
216 Path::new()
217 .set("fill", "none")
218 .set("stroke", color)
219 .set("d", path_data)
220}
221
222fn generate_side_panel_outline_path(
223 starting_point_x: f32,
224 starting_point_y: f32,
225 dimensions: &ContainerDimensions,
226 rows: usize,
227 material_thickness: f32,
228 color: &str,
229) -> Path {
230 let panel_inner_height = (dimensions.height * rows) as f32;
231 let side_panel_path_data = Data::new()
232 .move_to((starting_point_x + material_thickness, starting_point_y))
233 .vertical_line_to(starting_point_y + SIDE_TAP_FROM_FRONT as f32)
234 .horizontal_line_to(starting_point_x)
235 .vertical_line_to(starting_point_y + (SIDE_TAP_FROM_FRONT + SIDE_TAP_WIDTH) as f32)
236 .horizontal_line_to(starting_point_x + material_thickness)
237 .vertical_line_to(
238 starting_point_y + (dimensions.depth - SIDE_TAP_FROM_FRONT - SIDE_TAP_WIDTH) as f32,
239 )
240 .horizontal_line_to(starting_point_x)
241 .vertical_line_to(starting_point_y + (dimensions.depth - SIDE_TAP_FROM_FRONT) as f32)
242 .horizontal_line_to(starting_point_x + material_thickness)
243 .vertical_line_to(starting_point_y + dimensions.depth as f32)
244 .horizontal_line_to(starting_point_x + panel_inner_height + (1.0 * material_thickness))
245 .vertical_line_to(starting_point_y + (dimensions.depth - SIDE_TAP_FROM_FRONT) as f32)
246 .horizontal_line_to(starting_point_x + panel_inner_height + (2.0 * material_thickness))
247 .vertical_line_to(
248 starting_point_y + (dimensions.depth - SIDE_TAP_FROM_FRONT - SIDE_TAP_WIDTH) as f32,
249 )
250 .horizontal_line_to(starting_point_x + panel_inner_height + (1.0 * material_thickness))
251 .vertical_line_to(starting_point_y + (SIDE_TAP_FROM_FRONT + SIDE_TAP_WIDTH) as f32)
252 .horizontal_line_to(starting_point_x + panel_inner_height + (2.0 * material_thickness))
253 .vertical_line_to(starting_point_y + SIDE_TAP_FROM_FRONT as f32)
254 .horizontal_line_to(starting_point_x + panel_inner_height + (1.0 * material_thickness))
255 .vertical_line_to(starting_point_y)
256 .close();
257
258 Path::new()
259 .set("fill", "none")
260 .set("stroke", color)
261 .set("d", side_panel_path_data)
262}
263
264fn generate_top_and_bottom_pieces(
265 document: &mut Document,
266 dimensions: &ContainerDimensions,
267 starting_point_x: f32,
268 columns: usize,
269 column_width: f32,
270 material_thickness: f32,
271 primary_color: &str,
272 secondary_color: &str,
273) {
274 generate_cover_path(
275 document,
276 dimensions,
277 starting_point_x,
278 0.0,
279 columns,
280 column_width,
281 material_thickness,
282 primary_color,
283 secondary_color,
284 );
285
286 generate_cover_path(
287 document,
288 dimensions,
289 starting_point_x,
290 (dimensions.depth + CLEARANCE_BETWEEN_PATHS) as f32,
291 columns,
292 column_width,
293 material_thickness,
294 primary_color,
295 secondary_color,
296 );
297}
298
299fn generate_cover_path(
300 document: &mut Document,
301 dimensions: &ContainerDimensions,
302 starting_point_x: f32,
303 starting_point_y: f32,
304 columns: usize,
305 column_width: f32,
306 material_thickness: f32,
307 primary_color: &str,
308 secondary_color: &str,
309) {
310 let top_path_data = generate_top_path(
312 dimensions,
313 starting_point_x,
314 starting_point_y,
315 columns,
316 column_width,
317 material_thickness,
318 );
319 let path = Path::new()
320 .set("fill", "none")
321 .set("stroke", secondary_color)
322 .set("d", top_path_data);
323 document.append(path);
324
325 for i in 0..columns - 1 {
326 let x = starting_point_x + column_width + (i as f32 * column_width);
327 let y = starting_point_y + SIDE_TAP_FROM_FRONT as f32;
328 let side_tap_hole_path = generate_side_tap_path(x, y, material_thickness, primary_color);
329 document.append(side_tap_hole_path);
330
331 let side_tap_hole_path = generate_side_tap_path(
332 x,
333 y + (dimensions.depth - SIDE_TAP_FROM_FRONT - (SIDE_TAP_WIDTH * 2)) as f32,
334 material_thickness,
335 primary_color,
336 );
337 document.append(side_tap_hole_path);
338 }
339
340 }
342
343fn generate_side_tap_path(x: f32, y: f32, material_thickness: f32, color: &str) -> Path {
344 let data = Data::new()
345 .move_to((x, y))
346 .vertical_line_to(y + SIDE_TAP_WIDTH as f32)
347 .horizontal_line_to(x + material_thickness as f32)
348 .vertical_line_to(y)
349 .close();
350
351 Path::new()
352 .set("fill", "none")
353 .set("stroke", color)
354 .set("d", data)
355}
356
357fn generate_top_path(
358 dimensions: &ContainerDimensions,
359 starting_point_x: f32,
360 starting_point_y: f32,
361 columns: usize,
362 column_width: f32,
363 material_thickness: f32,
364) -> Data {
365 let top_width = top_width(column_width, columns, material_thickness);
366
367 Data::new()
368 .move_to((starting_point_x, starting_point_y))
369 .vertical_line_to(starting_point_y + SIDE_TAP_FROM_FRONT as f32)
370 .horizontal_line_to(starting_point_x + material_thickness)
371 .vertical_line_to(starting_point_y + (SIDE_TAP_FROM_FRONT + SIDE_TAP_WIDTH) as f32)
372 .horizontal_line_to(starting_point_x)
373 .vertical_line_to(
374 starting_point_y + (dimensions.depth - (SIDE_TAP_FROM_FRONT + SIDE_TAP_WIDTH)) as f32,
375 )
376 .horizontal_line_to(starting_point_x + material_thickness)
377 .vertical_line_to(starting_point_y + (dimensions.depth - SIDE_TAP_FROM_FRONT) as f32)
378 .horizontal_line_to(starting_point_x)
379 .vertical_line_to(starting_point_y + dimensions.depth as f32)
380 .horizontal_line_to(starting_point_x + top_width)
381 .vertical_line_to(starting_point_y + (dimensions.depth - SIDE_TAP_FROM_FRONT) as f32)
382 .horizontal_line_to(starting_point_x - material_thickness + top_width)
383 .vertical_line_to(
384 starting_point_y + (dimensions.depth - (SIDE_TAP_FROM_FRONT + SIDE_TAP_WIDTH)) as f32,
385 )
386 .horizontal_line_to(starting_point_x + top_width)
387 .vertical_line_to(starting_point_y + (SIDE_TAP_FROM_FRONT + SIDE_TAP_WIDTH) as f32)
388 .horizontal_line_to(starting_point_x - material_thickness + top_width)
389 .vertical_line_to(starting_point_y + SIDE_TAP_FROM_FRONT as f32)
390 .horizontal_line_to(starting_point_x + top_width)
391 .vertical_line_to(starting_point_y)
392 .close()
393}
394
395fn top_width(column_width: f32, columns: usize, material_thickness: f32) -> f32 {
396 (material_thickness + column_width * columns as f32) + material_thickness
397}
398fn generate_side_wing_pair(
399 document: &mut Document,
400 dimensions: &ContainerDimensions,
401 starting_point_x: f32,
402 starting_point_y: f32,
403 material_thickness: f32,
404 color: &str,
405) {
406 let path = generate_side_wing(
407 starting_point_x,
408 starting_point_y,
409 material_thickness,
410 dimensions.depth,
411 dimensions.side_wing_width,
412 false,
413 &color,
414 );
415 document.append(path);
416 let path = generate_side_wing(
417 starting_point_x,
418 starting_point_y + (dimensions.side_wing_width + CLEARANCE_BETWEEN_PATHS) as f32,
419 material_thickness,
420 dimensions.depth,
421 dimensions.side_wing_width,
422 true,
423 &color,
424 );
425 document.append(path);
426}
427
428fn height_of_two_side_wings(side_wing_width: usize, material_thickness: f32) -> f32 {
429 (side_wing_width * 2 + CLEARANCE_BETWEEN_PATHS) as f32 + material_thickness
430}
431
432fn generate_side_wing(
433 starting_point_x: f32,
434 starting_point_y: f32,
435 material_thickness: f32,
436 box_depth: usize,
437 box_side_wing_width: usize,
438 inverted: bool,
439 color: &str,
440) -> Path {
441 let wing_data = if inverted {
442 generate_side_wing_inverted_path(
443 starting_point_x,
444 starting_point_y,
445 material_thickness,
446 box_depth,
447 box_side_wing_width,
448 )
449 } else {
450 generate_side_wing_path(
451 starting_point_x,
452 starting_point_y,
453 material_thickness,
454 box_depth,
455 box_side_wing_width,
456 )
457 };
458
459 svg::node::element::Path::new()
460 .set("fill", "none")
461 .set("stroke", color)
462 .set("d", wing_data)
463}
464
465fn generate_side_wing_path(
466 starting_point_x: f32,
467 starting_point_y: f32,
468 material_thickness: f32,
469 box_depth: usize,
470 box_side_wing_width: usize,
471) -> Data {
472 Data::new()
473 .move_to((starting_point_x, starting_point_y))
474 .vertical_line_to(starting_point_y + box_side_wing_width as f32)
475 .horizontal_line_to(SIDE_WING_SLOT_FROM_FRONT)
476 .vertical_line_to(starting_point_y + material_thickness + box_side_wing_width as f32)
477 .horizontal_line_to(SIDE_WING_SLOT_FROM_FRONT + SIDE_WING_SLOT_WIDTH)
478 .vertical_line_to(starting_point_y + box_side_wing_width as f32)
479 .horizontal_line_to(third_side_wing_tap_position_from_front(box_depth))
480 .vertical_line_to(starting_point_y + box_side_wing_width as f32 + material_thickness)
481 .horizontal_line_to(
482 third_side_wing_tap_position_from_front(box_depth) + SIDE_WING_SLOT_WIDTH,
483 )
484 .vertical_line_to(starting_point_y + box_side_wing_width as f32)
485 .horizontal_line_to(box_depth)
486 .vertical_line_to(starting_point_y)
487 .close()
488}
489
490fn generate_side_wing_inverted_path(
491 starting_point_x: f32,
492 starting_point_y: f32,
493 material_thickness: f32,
494 box_depth: usize,
495 box_side_wing_width: usize,
496) -> Data {
497 Data::new()
498 .move_to((starting_point_x, starting_point_y + material_thickness))
499 .horizontal_line_to(second_side_wing_tap_position_from_front())
500 .vertical_line_to(starting_point_y)
501 .horizontal_line_to(second_side_wing_tap_position_from_front() + SIDE_WING_SLOT_WIDTH)
502 .vertical_line_to(starting_point_y + material_thickness)
503 .horizontal_line_to(fourth_side_wing_tap_position_from_front(box_depth))
504 .vertical_line_to(starting_point_y)
505 .horizontal_line_to(box_depth - SIDE_WING_SLOT_FROM_FRONT)
506 .vertical_line_to(starting_point_y + material_thickness)
507 .horizontal_line_to(box_depth)
508 .vertical_line_to(starting_point_y + material_thickness + box_side_wing_width as f32)
509 .horizontal_line_to(starting_point_x)
510 .close()
511}
512
513fn third_side_wing_tap_position_from_front(box_depth: usize) -> usize {
514 box_depth
515 - (SIDE_WING_SLOT_FROM_FRONT
516 + SIDE_WING_SLOT_WIDTH
517 + SIDE_WING_SLOT_SPACING
518 + SIDE_WING_SLOT_WIDTH)
519}
520fn second_side_wing_tap_position_from_front() -> usize {
521 SIDE_WING_SLOT_FROM_FRONT + SIDE_WING_SLOT_WIDTH + SIDE_WING_SLOT_SPACING
522}
523
524fn fourth_side_wing_tap_position_from_front(box_depth: usize) -> usize {
525 box_depth - (SIDE_WING_SLOT_FROM_FRONT + SIDE_WING_SLOT_WIDTH)
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use proptest::prelude::*;
532
533 proptest! {
536 #![proptest_config(ProptestConfig::with_cases(100))]
537
538 #[test]
539 fn test_assembled_width_formula(
540 columns in 1usize..=10,
541 container_width in 50usize..=500,
542 material_thickness in 1.0f32..=20.0,
543 ) {
544 let container = Container {
546 vendor: "Test".to_string(),
547 model: "Test".to_string(),
548 description: "Test".to_string(),
549 links: vec![],
550 dimensions: ContainerDimensions {
551 width: container_width,
552 depth: 100,
553 height: 100,
554 side_wing_from_box_top: 10,
555 side_wing_width: 20,
556 },
557 };
558
559 let result = generate_svg(
561 1, columns,
563 material_thickness,
564 &container,
565 "#000000",
566 "#FF0000",
567 );
568
569 let column_width = container_width + CLEARANCE_FOR_CONTAINER_WIDTH;
571 let expected_width = (column_width * columns) as f32
572 + (columns + 1) as f32 * material_thickness;
573
574 prop_assert_eq!(result.assembled_dimensions.width, expected_width);
576 }
577 }
578
579 proptest! {
582 #![proptest_config(ProptestConfig::with_cases(100))]
583
584 #[test]
585 fn test_assembled_height_formula(
586 rows in 1usize..=10,
587 container_height in 50usize..=500,
588 material_thickness in 1.0f32..=20.0,
589 ) {
590 let container = Container {
592 vendor: "Test".to_string(),
593 model: "Test".to_string(),
594 description: "Test".to_string(),
595 links: vec![],
596 dimensions: ContainerDimensions {
597 width: 100,
598 depth: 100,
599 height: container_height,
600 side_wing_from_box_top: 10,
601 side_wing_width: 20,
602 },
603 };
604
605 let result = generate_svg(
607 rows,
608 1, material_thickness,
610 &container,
611 "#000000",
612 "#FF0000",
613 );
614
615 let expected_height = (container_height * rows) as f32
617 + material_thickness * 2.0;
618
619 prop_assert_eq!(result.assembled_dimensions.height, expected_height);
621 }
622 }
623
624 proptest! {
627 #![proptest_config(ProptestConfig::with_cases(100))]
628
629 #[test]
630 fn test_assembled_depth_equals_container_depth(
631 rows in 1usize..=10,
632 columns in 1usize..=10,
633 container_depth in 100usize..=500,
634 material_thickness in 1.0f32..=20.0,
635 ) {
636 let container = Container {
638 vendor: "Test".to_string(),
639 model: "Test".to_string(),
640 description: "Test".to_string(),
641 links: vec![],
642 dimensions: ContainerDimensions {
643 width: 100,
644 depth: container_depth,
645 height: 100,
646 side_wing_from_box_top: 10,
647 side_wing_width: 20,
648 },
649 };
650
651 let result = generate_svg(
653 rows,
654 columns,
655 material_thickness,
656 &container,
657 "#000000",
658 "#FF0000",
659 );
660
661 let expected_depth = container_depth as f32;
664 prop_assert_eq!(result.assembled_dimensions.depth, expected_depth);
665 }
666 }
667
668 proptest! {
671 #![proptest_config(ProptestConfig::with_cases(100))]
672
673 #[test]
674 fn test_all_dimensions_positive(
675 rows in 1usize..=10,
676 columns in 1usize..=10,
677 container_width in 50usize..=500,
678 container_height in 50usize..=500,
679 container_depth in 100usize..=500,
680 material_thickness in 1.0f32..=20.0,
681 ) {
682 let container = Container {
684 vendor: "Test".to_string(),
685 model: "Test".to_string(),
686 description: "Test".to_string(),
687 links: vec![],
688 dimensions: ContainerDimensions {
689 width: container_width,
690 depth: container_depth,
691 height: container_height,
692 side_wing_from_box_top: 10,
693 side_wing_width: 20,
694 },
695 };
696
697 let result = generate_svg(
699 rows,
700 columns,
701 material_thickness,
702 &container,
703 "#000000",
704 "#FF0000",
705 );
706
707 prop_assert!(result.assembled_dimensions.width > 0.0,
709 "Width should be positive, got: {}", result.assembled_dimensions.width);
710 prop_assert!(result.assembled_dimensions.height > 0.0,
711 "Height should be positive, got: {}", result.assembled_dimensions.height);
712 prop_assert!(result.assembled_dimensions.depth > 0.0,
713 "Depth should be positive, got: {}", result.assembled_dimensions.depth);
714 }
715 }
716
717 proptest! {
720 #![proptest_config(ProptestConfig::with_cases(100))]
721
722 #[test]
723 fn test_calculation_idempotence(
724 rows in 1usize..=10,
725 columns in 1usize..=10,
726 container_width in 50usize..=500,
727 container_height in 50usize..=500,
728 container_depth in 100usize..=500,
729 material_thickness in 1.0f32..=20.0,
730 ) {
731 let container = Container {
733 vendor: "Test".to_string(),
734 model: "Test".to_string(),
735 description: "Test".to_string(),
736 links: vec![],
737 dimensions: ContainerDimensions {
738 width: container_width,
739 depth: container_depth,
740 height: container_height,
741 side_wing_from_box_top: 10,
742 side_wing_width: 20,
743 },
744 };
745
746 let result1 = generate_svg(
748 rows,
749 columns,
750 material_thickness,
751 &container,
752 "#000000",
753 "#FF0000",
754 );
755
756 let result2 = generate_svg(
757 rows,
758 columns,
759 material_thickness,
760 &container,
761 "#000000",
762 "#FF0000",
763 );
764
765 prop_assert_eq!(result1.assembled_dimensions.width, result2.assembled_dimensions.width,
767 "Width should be identical across calls");
768 prop_assert_eq!(result1.assembled_dimensions.height, result2.assembled_dimensions.height,
769 "Height should be identical across calls");
770 prop_assert_eq!(result1.assembled_dimensions.depth, result2.assembled_dimensions.depth,
771 "Depth should be identical across calls");
772 }
773 }
774
775 #[test]
779 fn test_known_values_2x3_rack() {
780 let container = Container {
782 vendor: "Test".to_string(),
783 model: "Test".to_string(),
784 description: "Test".to_string(),
785 links: vec![],
786 dimensions: ContainerDimensions {
787 width: 100,
788 depth: 100,
789 height: 100,
790 side_wing_from_box_top: 10,
791 side_wing_width: 20,
792 },
793 };
794
795 let result = generate_svg(2, 3, 5.0, &container, "#000000", "#FF0000");
796
797 let expected_width = 332.0;
800 let expected_height = 210.0;
802 let expected_depth = 100.0;
804
805 assert_eq!(result.assembled_dimensions.width, expected_width);
806 assert_eq!(result.assembled_dimensions.height, expected_height);
807 assert_eq!(result.assembled_dimensions.depth, expected_depth);
808 }
809
810 #[test]
811 fn test_minimum_configuration_1x1() {
812 let container = Container {
814 vendor: "Test".to_string(),
815 model: "Test".to_string(),
816 description: "Test".to_string(),
817 links: vec![],
818 dimensions: ContainerDimensions {
819 width: 80,
820 depth: 120,
821 height: 60,
822 side_wing_from_box_top: 10,
823 side_wing_width: 20,
824 },
825 };
826
827 let result = generate_svg(1, 1, 3.0, &container, "#000000", "#FF0000");
828
829 let expected_width = 90.0;
832 let expected_height = 66.0;
834 let expected_depth = 120.0;
836
837 assert_eq!(result.assembled_dimensions.width, expected_width);
838 assert_eq!(result.assembled_dimensions.height, expected_height);
839 assert_eq!(result.assembled_dimensions.depth, expected_depth);
840 }
841
842 #[test]
843 fn test_very_small_material_thickness() {
844 let container = Container {
846 vendor: "Test".to_string(),
847 model: "Test".to_string(),
848 description: "Test".to_string(),
849 links: vec![],
850 dimensions: ContainerDimensions {
851 width: 100,
852 depth: 100,
853 height: 100,
854 side_wing_from_box_top: 10,
855 side_wing_width: 20,
856 },
857 };
858
859 let result = generate_svg(2, 2, 0.5, &container, "#000000", "#FF0000");
860
861 let expected_width = 209.5;
864 let expected_height = 201.0;
866 let expected_depth = 100.0;
868
869 assert_eq!(result.assembled_dimensions.width, expected_width);
870 assert_eq!(result.assembled_dimensions.height, expected_height);
871 assert_eq!(result.assembled_dimensions.depth, expected_depth);
872 }
873
874 #[test]
875 fn test_large_configuration() {
876 let container = Container {
878 vendor: "Test".to_string(),
879 model: "Test".to_string(),
880 description: "Test".to_string(),
881 links: vec![],
882 dimensions: ContainerDimensions {
883 width: 150,
884 depth: 200,
885 height: 120,
886 side_wing_from_box_top: 10,
887 side_wing_width: 20,
888 },
889 };
890
891 let result = generate_svg(5, 5, 6.0, &container, "#000000", "#FF0000");
892
893 let expected_width = 806.0;
896 let expected_height = 612.0;
898 let expected_depth = 200.0;
900
901 assert_eq!(result.assembled_dimensions.width, expected_width);
902 assert_eq!(result.assembled_dimensions.height, expected_height);
903 assert_eq!(result.assembled_dimensions.depth, expected_depth);
904 }
905
906 #[test]
907 fn test_single_row_multiple_columns() {
908 let container = Container {
910 vendor: "Test".to_string(),
911 model: "Test".to_string(),
912 description: "Test".to_string(),
913 links: vec![],
914 dimensions: ContainerDimensions {
915 width: 90,
916 depth: 110,
917 height: 70,
918 side_wing_from_box_top: 10,
919 side_wing_width: 20,
920 },
921 };
922
923 let result = generate_svg(1, 4, 4.0, &container, "#000000", "#FF0000");
924
925 let expected_width = 396.0;
928 let expected_height = 78.0;
930 let expected_depth = 110.0;
932
933 assert_eq!(result.assembled_dimensions.width, expected_width);
934 assert_eq!(result.assembled_dimensions.height, expected_height);
935 assert_eq!(result.assembled_dimensions.depth, expected_depth);
936 }
937
938 #[test]
939 fn test_multiple_rows_single_column() {
940 let container = Container {
942 vendor: "Test".to_string(),
943 model: "Test".to_string(),
944 description: "Test".to_string(),
945 links: vec![],
946 dimensions: ContainerDimensions {
947 width: 85,
948 depth: 95,
949 height: 65,
950 side_wing_from_box_top: 10,
951 side_wing_width: 20,
952 },
953 };
954
955 let result = generate_svg(4, 1, 3.5, &container, "#000000", "#FF0000");
956
957 let expected_width = 96.0;
960 let expected_height = 267.0;
962 let expected_depth = 95.0;
964
965 assert_eq!(result.assembled_dimensions.width, expected_width);
966 assert_eq!(result.assembled_dimensions.height, expected_height);
967 assert_eq!(result.assembled_dimensions.depth, expected_depth);
968 }
969}