$fn=64; // --- Parameters --- can_diameter = 66.5; // Standard spray can diameter (with slight clearance) wall_thickness = 2.5; // Thickness of the holder walls num_cans = 3; // Number of cans the rack will hold tube_length = 90; // Length of the holding tubes tilt_angle = 35; // Upward tilt angle of the cans (degrees) backplate_thickness = 5; // Thickness of the main backplate spacing = 2; // Spacing between can holders // ArtArsenal / Wall Rail Mount Settings rail_depth = 8; // Depth of the wall rail cleat rail_height = 15; // Height of the wall rail cleat // --- Calculated Values --- tube_od = can_diameter + (wall_thickness * 2); total_width = (tube_od * num_cans) + (spacing * (num_cans + 1)); backplate_height = 120; z_center = 30; // Base height where tubes attach to the backplate module spray_rack() { difference() { union() { // Main backplate cube([total_width, backplate_thickness, backplate_height]); // Top mounting cleat (ArtArsenal / French Cleat style downward hook) translate([0, -rail_depth, backplate_height - rail_height]) hull() { // Top front (against backplate) translate([0, rail_depth - 0.1, rail_height - 0.1]) cube([total_width, 0.1, 0.1]); // Top back translate([0, 0, rail_height - 0.1]) cube([total_width, 0.1, 0.1]); // Bottom back (the downward point of the hook) translate([0, 0, 0]) cube([total_width, 0.1, 0.1]); // Inner corner (creates the 45-degree upward angle to the backplate) translate([0, rail_depth - 0.1, rail_depth]) cube([total_width, 0.1, 0.1]); } // Bottom standoff spacer to keep the rack vertically aligned translate([0, -rail_depth, 0]) cube([total_width, rail_depth, 10]); // Can holder tubes and their base transitions for(i=[0:num_cans-1]) { x_pos = spacing + tube_od/2 + i*(tube_od+spacing); // Main outer tube translate([x_pos, backplate_thickness, z_center]) rotate([-tilt_angle, 0, 0]) cylinder(h=tube_length, d=tube_od); // Solid base transition connecting the tube to the backplate hull() { translate([x_pos, backplate_thickness, z_center]) rotate([-tilt_angle, 0, 0]) cylinder(h=15, d=tube_od); translate([x_pos - tube_od/2, 0, z_center - tube_od/2]) cube([tube_od, backplate_thickness, tube_od]); } } } // --- Cutouts --- for(i=[0:num_cans-1]) { x_pos = spacing + tube_od/2 + i*(tube_od+spacing); // Inner cylindrical hole for the can translate([x_pos, backplate_thickness, z_center]) rotate([-tilt_angle, 0, 0]) translate([0, 0, wall_thickness]) cylinder(h=tube_length + 10, d=can_diameter); // Front visibility slot (shows spray can colors/labels) slot_width = can_diameter * 0.75; translate([x_pos, backplate_thickness, z_center]) rotate([-tilt_angle, 0, 0]) translate([-slot_width/2, can_diameter/4, 25]) // Leaves a 25mm deep cup at the bottom cube([slot_width, tube_od, tube_length]); } // Clean up any geometry that protrudes below the flat bottom (Z < 0) translate([-10, -50, -50]) cube([total_width + 20, 200, 50]); } } // Render the final part spray_rack();