Skip to main content

container_rack_lib/rack/
mod.rs

1use svg::node::element::path::Data;
2use svg::node::element::Path;
3use svg::{Document, Node};
4use url::Url;
5
6// All measurements are in mm
7const 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 container_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    // Generate side wings
93    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
105    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
117    generate_side_panels(
118        &mut document,
119        (container.dimensions.depth + CLEARANCE_BETWEEN_PATHS) as f32 //side wings
120            + top_width(column_width as f32, columns, material_thickness) + CLEARANCE_BETWEEN_PATHS as f32,
121        &container.dimensions, // top and bottom plates
122        rows,
123        columns,
124        material_thickness,
125        primary_color,
126        secondary_color,
127    );
128
129    // Calculate assembled dimensions
130    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        container_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    // Generate cover
311    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    //Generate side panel taps to middle of cover
341}
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    // Feature: calculate-assembled-dimensions, Property 1: Assembled Width Formula
534    // **Validates: Requirements 1.1, 1.4**
535    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            // Create a minimal container with the generated dimensions
545            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            // Generate SVG with test parameters
560            let result = generate_svg(
561                1, // rows (not relevant for width)
562                columns,
563                material_thickness,
564                &container,
565                "#000000",
566                "#FF0000",
567            );
568
569            // Calculate expected width using the formula
570            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            // Verify the assembled width matches the formula
575            prop_assert_eq!(result.container_dimensions.width, expected_width);
576        }
577    }
578
579    // Feature: calculate-assembled-dimensions, Property 2: Assembled Height Formula
580    // **Validates: Requirements 2.1, 2.4**
581    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            // Create a minimal container with the generated dimensions
591            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            // Generate SVG with test parameters
606            let result = generate_svg(
607                rows,
608                1, // columns (not relevant for height)
609                material_thickness,
610                &container,
611                "#000000",
612                "#FF0000",
613            );
614
615            // Calculate expected height using the formula
616            let expected_height = (container_height * rows) as f32 
617                + material_thickness * 2.0;
618
619            // Verify the assembled height matches the formula
620            prop_assert_eq!(result.container_dimensions.height, expected_height);
621        }
622    }
623
624    // Feature: calculate-assembled-dimensions, Property 3: Assembled Depth Equals Container Depth
625    // **Validates: Requirements 3.1, 3.4**
626    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            // Create a minimal container with the generated dimensions
637            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            // Generate SVG with test parameters
652            let result = generate_svg(
653                rows,
654                columns,
655                material_thickness,
656                &container,
657                "#000000",
658                "#FF0000",
659            );
660
661            // Verify the assembled depth equals container depth
662            // regardless of rows, columns, or material thickness
663            let expected_depth = container_depth as f32;
664            prop_assert_eq!(result.container_dimensions.depth, expected_depth);
665        }
666    }
667
668    // Feature: calculate-assembled-dimensions, Property 4: All Dimensions Positive
669    // **Validates: Requirements 1.3, 2.3, 3.3, 4.2**
670    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            // Create a container with all positive input dimensions
683            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            // Generate SVG with test parameters
698            let result = generate_svg(
699                rows,
700                columns,
701                material_thickness,
702                &container,
703                "#000000",
704                "#FF0000",
705            );
706
707            // Verify all three dimensions are positive
708            prop_assert!(result.container_dimensions.width > 0.0, 
709                "Width should be positive, got: {}", result.container_dimensions.width);
710            prop_assert!(result.container_dimensions.height > 0.0,
711                "Height should be positive, got: {}", result.container_dimensions.height);
712            prop_assert!(result.container_dimensions.depth > 0.0,
713                "Depth should be positive, got: {}", result.container_dimensions.depth);
714        }
715    }
716
717    // Feature: calculate-assembled-dimensions, Property 5: Calculation Idempotence
718    // **Validates: Requirements 6.4**
719    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            // Create a container with random dimensions
732            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            // Call generate_svg twice with identical inputs
747            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            // Verify both GeneratedSvg structures contain identical dimension values
766            prop_assert_eq!(result1.container_dimensions.width, result2.container_dimensions.width,
767                "Width should be identical across calls");
768            prop_assert_eq!(result1.container_dimensions.height, result2.container_dimensions.height,
769                "Height should be identical across calls");
770            prop_assert_eq!(result1.container_dimensions.depth, result2.container_dimensions.depth,
771                "Depth should be identical across calls");
772        }
773    }
774
775    // Unit tests for specific dimension calculations
776    // **Validates: Requirements 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3**
777
778    #[test]
779    fn test_known_values_2x3_rack() {
780        // Test with known values: 2×3 rack with 100mm containers and 5mm material
781        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        // Calculate expected values
798        // Width: (100 + 4) * 3 + (3 + 1) * 5 = 312 + 20 = 332mm
799        let expected_width = 332.0;
800        // Height: 100 * 2 + 5 * 2 = 200 + 10 = 210mm
801        let expected_height = 210.0;
802        // Depth: 100mm (unchanged)
803        let expected_depth = 100.0;
804
805        assert_eq!(result.container_dimensions.width, expected_width);
806        assert_eq!(result.container_dimensions.height, expected_height);
807        assert_eq!(result.container_dimensions.depth, expected_depth);
808    }
809
810    #[test]
811    fn test_minimum_configuration_1x1() {
812        // Test minimum configuration: 1×1 rack
813        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        // Calculate expected values
830        // Width: (80 + 4) * 1 + (1 + 1) * 3 = 84 + 6 = 90mm
831        let expected_width = 90.0;
832        // Height: 60 * 1 + 3 * 2 = 60 + 6 = 66mm
833        let expected_height = 66.0;
834        // Depth: 120mm (unchanged)
835        let expected_depth = 120.0;
836
837        assert_eq!(result.container_dimensions.width, expected_width);
838        assert_eq!(result.container_dimensions.height, expected_height);
839        assert_eq!(result.container_dimensions.depth, expected_depth);
840    }
841
842    #[test]
843    fn test_very_small_material_thickness() {
844        // Test edge case: very small material thickness
845        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        // Calculate expected values
862        // Width: (100 + 4) * 2 + (2 + 1) * 0.5 = 208 + 1.5 = 209.5mm
863        let expected_width = 209.5;
864        // Height: 100 * 2 + 0.5 * 2 = 200 + 1.0 = 201mm
865        let expected_height = 201.0;
866        // Depth: 100mm (unchanged)
867        let expected_depth = 100.0;
868
869        assert_eq!(result.container_dimensions.width, expected_width);
870        assert_eq!(result.container_dimensions.height, expected_height);
871        assert_eq!(result.container_dimensions.depth, expected_depth);
872    }
873
874    #[test]
875    fn test_large_configuration() {
876        // Test edge case: large configuration (5×5 rack)
877        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        // Calculate expected values
894        // Width: (150 + 4) * 5 + (5 + 1) * 6 = 770 + 36 = 806mm
895        let expected_width = 806.0;
896        // Height: 120 * 5 + 6 * 2 = 600 + 12 = 612mm
897        let expected_height = 612.0;
898        // Depth: 200mm (unchanged)
899        let expected_depth = 200.0;
900
901        assert_eq!(result.container_dimensions.width, expected_width);
902        assert_eq!(result.container_dimensions.height, expected_height);
903        assert_eq!(result.container_dimensions.depth, expected_depth);
904    }
905
906    #[test]
907    fn test_single_row_multiple_columns() {
908        // Test edge case: single row with multiple columns
909        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        // Calculate expected values
926        // Width: (90 + 4) * 4 + (4 + 1) * 4 = 376 + 20 = 396mm
927        let expected_width = 396.0;
928        // Height: 70 * 1 + 4 * 2 = 70 + 8 = 78mm
929        let expected_height = 78.0;
930        // Depth: 110mm (unchanged)
931        let expected_depth = 110.0;
932
933        assert_eq!(result.container_dimensions.width, expected_width);
934        assert_eq!(result.container_dimensions.height, expected_height);
935        assert_eq!(result.container_dimensions.depth, expected_depth);
936    }
937
938    #[test]
939    fn test_multiple_rows_single_column() {
940        // Test edge case: multiple rows with single column
941        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        // Calculate expected values
958        // Width: (85 + 4) * 1 + (1 + 1) * 3.5 = 89 + 7 = 96mm
959        let expected_width = 96.0;
960        // Height: 65 * 4 + 3.5 * 2 = 260 + 7 = 267mm
961        let expected_height = 267.0;
962        // Depth: 95mm (unchanged)
963        let expected_depth = 95.0;
964
965        assert_eq!(result.container_dimensions.width, expected_width);
966        assert_eq!(result.container_dimensions.height, expected_height);
967        assert_eq!(result.container_dimensions.depth, expected_depth);
968    }
969}