/* * Parametric enclosure for the Adafruit ESP32 Feather V2 and FeatherWings. * * Coordinate system: * X: USB-C wall (0) toward the rear of the case * Y: non-battery side (0) toward the battery/JST side * Z: outside bottom (0) upward * * Board coordinates are derived from Adafruit's Eagle PCB files: * https://github.com/adafruit/Adafruit-ESP32-Feather-V2-PCB * https://github.com/adafruit/Adafruit-OLED-FeatherWing-PCB */ /* [Output] */ part = "assembly"; // [assembly,body,lid,buttons,insert_coupon,switch_coupon,fit_coupon] exploded_view = 8; // [0:1:20] show_references = true; /* [Stack] */ top_profile = "generic"; // [generic,gps,oled128x64] wing_count = 1; // [1:1:8] wing_pitch = 12.25; // [7:0.1:16] stack_height_override = 0; // [0:0.1:100] generic_component_height = 3; // [0:0.1:30] generic_top_gap = 2; // [0.5:0.1:10] gps_antenna_height = 6; gps_top_gap = 5; oled_component_height = 2.4; oled_display_height = 1.5; oled_face_gap = 0.8; /* [OLED controls] */ button_a = true; button_b = true; button_c = true; button_reset = false; button_projection = 1.5; // [0.5:0.1:3] button_cap_size = [5, 3.2]; button_flange_size = [6.6, 3.6]; button_flange_thickness = 0.9; button_travel = 0.5; button_contact_extension = 0.7; /* [Battery and switch] */ battery_enabled = false; battery_length = 69; battery_diameter = 18; battery_radial_clearance = 0.6; battery_end_clearance = 1; switch_hole_diameter = 12; switch_hole_compensation = 0.3; switch_cap_diameter = 8; switch_flange_diameter = 16.2; switch_body_length = 24; switch_body_diameter = 12; switch_wire_allowance = 3; /* [Tripod mount] */ tripod_insert_enabled = false; tripod_insert_length = 6.4; tripod_insert_hole_diameter = 8.8; tripod_thread_clearance_diameter = 6.8; tripod_boss_diameter = 14; /* [M2.5 inserts] */ m25_insert_length = 4; m25_insert_hole_diameter = 3.7; m25_screw_clearance = 2.8; m25_boss_diameter = 7; insert_entry_chamfer = 0.6; insert_melt_relief = 0.8; /* [Shell and fit] */ wall_thickness = 2.4; floor_thickness = 2.4; lid_thickness = 3.2; corner_radius = 4; board_bottom_clearance = 5.4; board_side_clearance = 7; board_rear_clearance = 4; usb_board_wall_gap = 2; lid_lip_depth = 2.4; lid_lip_thickness = 1.2; fit_clearance = 0.35; cutout_min_web = 1.2; hole_compensation = 0.2; lid_screw_head_diameter = 5.2; lid_screw_head_height = 2.5; /* [USB-C opening] */ usb_opening_enabled = true; usb_guide_enabled = true; usb_connector_overhang = 2; usb_guide_wall_thickness = 1.2; usb_guide_end_gap = 1; usb_opening_width = 12.5; usb_opening_height = 7; usb_opening_radius = 1.5; usb_vertical_offset = 0; /* [Custom wall cutouts] */ // Entry format: [face, shape, u, z, width, height, radius] // face: front, rear, left, right. u is Y on front/rear and X on left/right. // shape: circle, rect, roundrect, slot. Circle uses width as its diameter. extra_cutouts = []; /* [Calibration] */ coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3]; $fn = $preview ? 36 : 72; eps = 0.02; // Adafruit Feather mechanical reference, millimetres. feather_length = 50.8; feather_width = 22.86; board_thickness = 1.6; mount_x = [2.54, 48.26]; mount_y = [2.54, 20.32]; usb_center_y = 11.43; // Official 128x64 OLED FeatherWing geometry in Feather-board coordinates. oled_visible_min = [11.42, 3.78]; oled_visible_size = [30, 15.3]; oled_button_a_xy = [4.318, 15.875]; oled_button_b_xy = [4.318, 11.43]; oled_button_c_xy = [4.318, 6.985]; oled_button_reset_xy = [7.366, 20.828]; effective_floor = tripod_insert_enabled ? max(floor_thickness, tripod_insert_length + insert_melt_relief + 1.2) : floor_thickness; effective_board_clearance = max( board_bottom_clearance, m25_insert_length + insert_melt_relief + 0.6 ); board_origin_x = wall_thickness + usb_board_wall_gap; board_origin_y = wall_thickness + board_side_clearance; board_bottom_z = effective_floor + effective_board_clearance; board_top_z = board_bottom_z + board_thickness; usb_center_u = board_origin_y + usb_center_y; usb_center_z = board_top_z + 1.6 + usb_vertical_offset; usb_inner_width = usb_opening_width + hole_compensation; usb_inner_height = usb_opening_height + hole_compensation; usb_guide_length = max( 0, usb_board_wall_gap - usb_connector_overhang - usb_guide_end_gap ); usb_guide_active = usb_opening_enabled && usb_guide_enabled && usb_guide_length > 0; usb_reserved_width = usb_inner_width + (usb_guide_active ? 2 * usb_guide_wall_thickness : 0); usb_reserved_height = usb_inner_height + (usb_guide_active ? 2 * usb_guide_wall_thickness : 0); computed_top_pcb_z = board_top_z + wing_count * wing_pitch; top_pcb_z = stack_height_override > 0 ? board_top_z + stack_height_override : computed_top_pcb_z; profile_component_height = top_profile == "gps" ? gps_antenna_height : top_profile == "oled128x64" ? oled_component_height : generic_component_height; profile_top_gap = top_profile == "gps" ? gps_top_gap : top_profile == "oled128x64" ? oled_face_gap : generic_top_gap; partition_y = board_origin_y + feather_width + 1; battery_center_y = partition_y + wall_thickness + battery_radial_clearance + battery_diameter / 2; battery_start_x = board_origin_x + battery_end_clearance; battery_end_x = battery_start_x + battery_length; switch_center_x = battery_end_x + max( switch_body_diameter / 2 + 0.8, switch_flange_diameter / 2 + 0.5 ); case_length_plain = board_origin_x + feather_length + board_rear_clearance + wall_thickness; case_length_battery = switch_center_x + switch_flange_diameter / 2 + 1.2 + wall_thickness; case_length = battery_enabled ? max(case_length_plain, case_length_battery) : case_length_plain; case_width_plain = board_origin_y + feather_width + board_side_clearance + wall_thickness; case_width_battery = battery_center_y + battery_diameter / 2 + battery_radial_clearance + wall_thickness; case_width = battery_enabled ? max(case_width_plain, case_width_battery) : case_width_plain; battery_center_z = effective_floor + battery_radial_clearance + battery_diameter / 2; switch_center_z = max( effective_floor + switch_flange_diameter / 2 + 1, battery_center_z ); electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap; battery_ceiling_z = battery_enabled ? max( battery_center_z + battery_diameter / 2 + battery_radial_clearance, switch_center_z + switch_flange_diameter / 2 + 1 ) : 0; body_height = max(electronics_ceiling_z, battery_ceiling_z); lid_boss_r = m25_boss_diameter / 2; lid_boss_wall_overlap = 0.6; lid_boss_xy = [ [ wall_thickness + lid_boss_r - lid_boss_wall_overlap, wall_thickness + lid_boss_r - lid_boss_wall_overlap ], [ case_length - wall_thickness - lid_boss_r + lid_boss_wall_overlap, wall_thickness + lid_boss_r - lid_boss_wall_overlap ], [ wall_thickness + lid_boss_r - lid_boss_wall_overlap, case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap ], [ case_length - wall_thickness - lid_boss_r + lid_boss_wall_overlap, case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap ] ]; assert(wing_count >= 1, "wing_count must be at least one"); assert(top_profile == "generic" || top_profile == "gps" || top_profile == "oled128x64", "Unsupported top_profile"); assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM"); assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines"); assert(m25_boss_diameter > m25_insert_hole_diameter + 2, "M2.5 boss does not have enough radial material"); assert(!tripod_insert_enabled || tripod_boss_diameter > tripod_insert_hole_diameter + 3, "Tripod insert boss does not have enough radial material"); assert(usb_opening_enabled || !usb_guide_enabled, "USB guide requires the USB opening"); function clamp(v, lo, hi) = min(max(v, lo), hi); function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4]; function cutouts_separated(first, second, web) = abs(first[2] - second[2]) >= (first[4] + second[4]) / 2 + web || abs(first[3] - second[3]) >= (cutout_height(first) + cutout_height(second)) / 2 + web; function cutout_clear_of_usb(entry, web) = abs(entry[2] - usb_center_u) >= (entry[4] + usb_reserved_width) / 2 + web || abs(entry[3] - usb_center_z) >= (cutout_height(entry) + usb_reserved_height) / 2 + web; module validate_cutout_spacing() { if (len(extra_cutouts) > 1) for (first = [0 : len(extra_cutouts) - 2], second = [first + 1 : len(extra_cutouts) - 1]) if (extra_cutouts[first][0] == extra_cutouts[second][0]) assert( cutouts_separated( extra_cutouts[first], extra_cutouts[second], cutout_min_web ), "Custom cutouts are too close on the same wall" ); if (usb_opening_enabled) for (entry = extra_cutouts) if (entry[0] == "front") assert( cutout_clear_of_usb(entry, cutout_min_web), "Front cutout is too close to the USB opening or guide" ); } module rounded_rect_2d(size, radius) { r = clamp(radius, 0.01, min(size[0], size[1]) / 2 - 0.01); hull() { for (x = [r, size[0] - r], y = [r, size[1] - r]) translate([x, y]) circle(r = r); } } module centered_rounded_rect_2d(size, radius) { translate([-size[0] / 2, -size[1] / 2]) rounded_rect_2d(size, radius); } module centered_rounded_prism(size, height, radius) { linear_extrude(height = height) centered_rounded_rect_2d(size, radius); } module rounded_box(size, radius) { linear_extrude(height = size[2]) rounded_rect_2d([size[0], size[1]], radius); } module insert_pocket(diameter, depth, entry_z, downward = true) { bore_d = diameter + hole_compensation; if (downward) { translate([0, 0, entry_z - depth]) cylinder(h = depth + eps, d = bore_d); translate([0, 0, entry_z - insert_entry_chamfer]) cylinder( h = insert_entry_chamfer + eps, d1 = bore_d, d2 = bore_d + 2 * insert_entry_chamfer ); } else { translate([0, 0, entry_z - eps]) cylinder(h = depth + eps, d = bore_d); translate([0, 0, entry_z - eps]) cylinder( h = insert_entry_chamfer + eps, d1 = bore_d + 2 * insert_entry_chamfer, d2 = bore_d ); } } module face_rounded_cutout(face, u, z, width, height, radius) { r = clamp(radius, 0.01, min(width, height) / 2); depth = wall_thickness + 2 * eps; if (face == "front" || face == "rear") { x0 = face == "front" ? -eps : case_length - wall_thickness - eps; hull() for (yy = [u - width / 2 + r, u + width / 2 - r], zz = [z - height / 2 + r, z + height / 2 - r]) translate([x0, yy, zz]) rotate([0, 90, 0]) cylinder(h = depth, r = r); } else if (face == "left" || face == "right") { y0 = face == "left" ? -eps : case_width - wall_thickness - eps; hull() for (xx = [u - width / 2 + r, u + width / 2 - r], zz = [z - height / 2 + r, z + height / 2 - r]) translate([xx, y0, zz]) rotate([-90, 0, 0]) cylinder(h = depth, r = r); } } module face_circle_cutout(face, u, z, diameter) { depth = wall_thickness + 2 * eps; if (face == "front") translate([-eps, u, z]) rotate([0, 90, 0]) cylinder(h = depth, d = diameter); else if (face == "rear") translate([case_length - wall_thickness - eps, u, z]) rotate([0, 90, 0]) cylinder(h = depth, d = diameter); else if (face == "left") translate([u, -eps, z]) rotate([-90, 0, 0]) cylinder(h = depth, d = diameter); else if (face == "right") translate([u, case_width - wall_thickness - eps, z]) rotate([-90, 0, 0]) cylinder(h = depth, d = diameter); } module x_rounded_prism(x, length, u, z, width, height, radius) { r = clamp(radius, 0.01, min(width, height) / 2 - 0.01); hull() for (yy = [u - width / 2 + r, u + width / 2 - r], zz = [z - height / 2 + r, z + height / 2 - r]) translate([x, yy, zz]) rotate([0, 90, 0]) cylinder(h = length, r = r); } module usb_guide_shell() { outer_width = usb_inner_width + 2 * usb_guide_wall_thickness; outer_height = usb_inner_height + 2 * usb_guide_wall_thickness; difference() { x_rounded_prism( wall_thickness - eps, usb_guide_length + 2 * eps, usb_center_u, usb_center_z, outer_width, outer_height, usb_opening_radius + usb_guide_wall_thickness ); x_rounded_prism( wall_thickness - 2 * eps, usb_guide_length + 4 * eps, usb_center_u, usb_center_z, usb_inner_width, usb_inner_height, usb_opening_radius ); } } module custom_wall_cutout(entry) { face = entry[0]; shape = entry[1]; u = entry[2]; z = entry[3]; width = entry[4]; height = len(entry) > 5 ? entry[5] : width; radius = len(entry) > 6 ? entry[6] : min(width, height) / 2; assert(face == "front" || face == "rear" || face == "left" || face == "right", "Invalid cutout face"); assert(shape == "circle" || shape == "rect" || shape == "roundrect" || shape == "slot", "Invalid cutout shape"); assert(z - height / 2 >= effective_floor, "Cutout intersects the floor"); assert(z + height / 2 <= body_height - lid_lip_depth, "Cutout intersects the lid locating lip"); assert( (face == "front" || face == "rear") ? u - width / 2 >= 0 && u + width / 2 <= case_width : u - width / 2 >= 0 && u + width / 2 <= case_length, "Cutout extends beyond its wall" ); if (shape == "circle") face_circle_cutout(face, u, z, width); else face_rounded_cutout( face, u, z, width, height, shape == "rect" ? 0.02 : shape == "slot" ? min(width, height) / 2 : radius ); } module board_standoffs() { for (mx = mount_x, my = mount_y) translate([ board_origin_x + mx, board_origin_y + my, effective_floor - eps ]) cylinder( h = effective_board_clearance + eps, d = m25_boss_diameter ); } module lid_bosses() { for (p = lid_boss_xy) translate([p[0], p[1], effective_floor - eps]) cylinder( h = body_height - effective_floor + eps, d = m25_boss_diameter ); } module battery_cradle() { partition_height = min( body_height - lid_lip_depth - 0.5, battery_center_z + battery_diameter / 2 + 1.5 ); rib_width = 2.4; rib_positions = [ battery_start_x + 4, (battery_start_x + battery_end_x) / 2, battery_end_x - 4 ]; // Board/battery partition, with a lead-routing notch near the JST end. difference() { translate([wall_thickness, partition_y, effective_floor - eps]) cube([ battery_end_x - wall_thickness + battery_end_clearance, wall_thickness, partition_height - effective_floor + eps ]); translate([ board_origin_x + 6.5, partition_y - eps, effective_floor + 1.5 ]) cube([9, wall_thickness + 2 * eps, 6]); } // Three low curved ribs cradle the protected cylindrical pack. for (x = rib_positions) difference() { translate([ x - rib_width / 2, battery_center_y - battery_diameter / 2 - battery_radial_clearance, effective_floor - eps ]) cube([ rib_width, battery_diameter + 2 * battery_radial_clearance, battery_diameter / 2 + battery_radial_clearance + eps ]); translate([x - rib_width, battery_center_y, battery_center_z]) rotate([0, 90, 0]) cylinder( h = rib_width * 2, d = battery_diameter + 2 * battery_radial_clearance ); } // End stops remain below the cell center so the lid can remove vertically. for (x = [ battery_start_x - battery_end_clearance - wall_thickness, battery_end_x + battery_end_clearance ]) translate([ x, battery_center_y - battery_diameter / 2 - battery_radial_clearance, effective_floor - eps ]) cube([ wall_thickness, battery_diameter + 2 * battery_radial_clearance, battery_diameter / 2 + 1 + eps ]); // Rear cross-strip separates the side-mounted switch body from the cell. translate([ battery_end_x + battery_end_clearance, partition_y, effective_floor - eps ]) cube([ case_length - wall_thickness - battery_end_x - battery_end_clearance, case_width - wall_thickness - partition_y, min(body_height - effective_floor, switch_center_z + switch_body_diameter / 2 + switch_wire_allowance - effective_floor) + eps ]); } module body_positive() { union() { difference() { rounded_box([case_length, case_width, body_height], corner_radius); translate([wall_thickness, wall_thickness, effective_floor]) rounded_box([ case_length - 2 * wall_thickness, case_width - 2 * wall_thickness, body_height - effective_floor + eps ], max(0.6, corner_radius - wall_thickness)); } board_standoffs(); lid_bosses(); if (battery_enabled) battery_cradle(); if (tripod_insert_enabled) translate([case_length / 2, case_width / 2, 0]) cylinder(h = effective_floor, d = tripod_boss_diameter); if (usb_guide_active) usb_guide_shell(); } } module body_cutouts() { // Board mounting insert pockets, loaded from above before the Feather. for (mx = mount_x, my = mount_y) translate([board_origin_x + mx, board_origin_y + my, 0]) insert_pocket( m25_insert_hole_diameter, m25_insert_length + insert_melt_relief, board_bottom_z ); // Lid insert pockets, loaded from the top of the body. for (p = lid_boss_xy) translate([p[0], p[1], 0]) insert_pocket( m25_insert_hole_diameter, m25_insert_length + insert_melt_relief, body_height ); // USB-C cable opening. Connector center is from the official ESP32 V2 PCB. if (usb_opening_enabled) face_rounded_cutout( "front", usb_center_u, usb_center_z, usb_inner_width, usb_inner_height, usb_opening_radius ); if (battery_enabled) { face_circle_cutout( "right", switch_center_x, switch_center_z, switch_hole_diameter + switch_hole_compensation ); // Clear the switch body and its prewired terminals through the rear bay. translate([ switch_center_x, case_width - wall_thickness + eps, switch_center_z ]) rotate([90, 0, 0]) cylinder( h = switch_body_length + switch_wire_allowance + wall_thickness + eps, d = switch_body_diameter + 2 * fit_clearance ); } if (tripod_insert_enabled) { translate([case_length / 2, case_width / 2, 0]) { translate([0, 0, -eps]) cylinder( h = effective_floor + 2 * eps, d = tripod_thread_clearance_diameter + hole_compensation ); insert_pocket( tripod_insert_hole_diameter, tripod_insert_length + insert_melt_relief, effective_floor ); } } for (entry = extra_cutouts) custom_wall_cutout(entry); } module body() { difference() { body_positive(); body_cutouts(); } } module lid_plate_positive() { union() { rounded_box([case_length, case_width, lid_thickness], corner_radius); // Inset rim: exported pointing upward so the outer lid face prints flat. translate([ wall_thickness + fit_clearance, wall_thickness + fit_clearance, lid_thickness ]) linear_extrude(height = lid_lip_depth) difference() { rounded_rect_2d([ case_length - 2 * (wall_thickness + fit_clearance), case_width - 2 * (wall_thickness + fit_clearance) ], max(0.6, corner_radius - wall_thickness)); translate([lid_lip_thickness, lid_lip_thickness]) rounded_rect_2d([ case_length - 2 * ( wall_thickness + fit_clearance + lid_lip_thickness ), case_width - 2 * ( wall_thickness + fit_clearance + lid_lip_thickness ) ], max(0.5, corner_radius - wall_thickness - lid_lip_thickness)); } } } module beveled_oled_window() { inner_size = oled_visible_size; outer_size = [oled_visible_size[0] + 3, oled_visible_size[1] + 3]; center = [ board_origin_x + oled_visible_min[0] + oled_visible_size[0] / 2, board_origin_y + oled_visible_min[1] + oled_visible_size[1] / 2 ]; hull() { translate([center[0], center[1], -eps]) linear_extrude(height = eps) centered_rounded_rect_2d(outer_size, 1.5); translate([center[0], center[1], lid_thickness - eps]) linear_extrude(height = 2 * eps) centered_rounded_rect_2d(inner_size, 0.8); } } module oled_button_lid_cutout(xy) { center = [board_origin_x + xy[0], board_origin_y + xy[1]]; shaft_size = [ button_cap_size[0] + fit_clearance * 2, button_cap_size[1] + fit_clearance * 2 ]; recess_size = [ button_flange_size[0] + fit_clearance * 2, button_flange_size[1] + fit_clearance * 2 ]; recess_depth = button_flange_thickness + button_travel; translate([center[0], center[1], -eps]) centered_rounded_prism( shaft_size, lid_thickness + 2 * eps, shaft_size[1] / 2 ); translate([center[0], center[1], lid_thickness - recess_depth]) centered_rounded_prism( recess_size, recess_depth + lid_lip_depth + eps, recess_size[1] / 2 ); } module lid_cutouts() { // Through holes plus shallow counterbores for flush M2.5 socket heads. for (p = lid_boss_xy) { translate([p[0], p[1], -eps]) cylinder( h = lid_thickness + lid_lip_depth + 2 * eps, d = m25_screw_clearance + hole_compensation ); translate([p[0], p[1], -eps]) cylinder( h = lid_screw_head_height + eps, d = lid_screw_head_diameter + hole_compensation ); } if (top_profile == "oled128x64") { beveled_oled_window(); if (button_a) oled_button_lid_cutout(oled_button_a_xy); if (button_b) oled_button_lid_cutout(oled_button_b_xy); if (button_c) oled_button_lid_cutout(oled_button_c_xy); if (button_reset) oled_button_lid_cutout(oled_button_reset_xy); } } module lid() { difference() { lid_plate_positive(); lid_cutouts(); } } module one_oled_button() { recess_depth = button_flange_thickness + button_travel; stem_height = lid_thickness - recess_depth + button_projection; // Print outer cap face down. The flange and contact nub build upward. centered_rounded_prism( button_cap_size, stem_height, button_cap_size[1] / 2 ); translate([0, 0, stem_height]) centered_rounded_prism( button_flange_size, button_flange_thickness, button_flange_size[1] / 2 ); translate([0, 0, stem_height + button_flange_thickness]) cylinder(h = button_contact_extension, d = 3); } module buttons() { enabled = [ button_a, button_b, button_c, button_reset ]; count_enabled = len([for (v = enabled) if (v) 1]); if (count_enabled == 0) echo("No OLED buttons are enabled."); for (i = [0 : len(enabled) - 1]) if (enabled[i]) translate([i * (button_flange_size[0] + 3), 0, 0]) one_oled_button(); } module reference_board() { color([0.05, 0.25, 0.55, 0.75]) translate([board_origin_x, board_origin_y, board_bottom_z]) rounded_box([feather_length, feather_width, board_thickness], 2.54); color([0.12, 0.45, 0.75, 0.45]) for (i = [1 : wing_count]) translate([ board_origin_x, board_origin_y, board_top_z + i * wing_pitch - board_thickness ]) rounded_box( [feather_length, feather_width, board_thickness], 2.54 ); } module reference_top_feature() { if (top_profile == "gps") color([0.92, 0.78, 0.35, 0.8]) translate([ board_origin_x + 31, board_origin_y + (feather_width - 15) / 2, top_pcb_z ]) cube([15, 15, gps_antenna_height]); else if (top_profile == "oled128x64") { color([0.12, 0.12, 0.12, 0.9]) translate([ board_origin_x + oled_visible_min[0], board_origin_y + oled_visible_min[1], top_pcb_z ]) cube([ oled_visible_size[0], oled_visible_size[1], oled_display_height ]); color([0.6, 0.6, 0.6, 0.9]) for (xy = [ oled_button_a_xy, oled_button_b_xy, oled_button_c_xy, oled_button_reset_xy ]) translate([ board_origin_x + xy[0], board_origin_y + xy[1], top_pcb_z ]) cylinder(h = 1.6, d = 2.1); } else color([0.3, 0.65, 0.35, 0.6]) translate([ board_origin_x + 10, board_origin_y + 4, top_pcb_z ]) cube([ feather_length - 20, feather_width - 8, generic_component_height ]); } module reference_battery_and_switch() { if (battery_enabled) { color([0.35, 0.18, 0.55, 0.75]) translate([battery_start_x, battery_center_y, battery_center_z]) rotate([0, 90, 0]) cylinder(h = battery_length, d = battery_diameter); color([0.8, 0.15, 0.1, 0.65]) translate([ switch_center_x, case_width - wall_thickness, switch_center_z ]) rotate([90, 0, 0]) { cylinder(h = switch_body_length, d = switch_body_diameter); translate([0, 0, -1.5]) cylinder(h = 1.5, d = switch_flange_diameter); translate([0, 0, -3]) cylinder(h = 3, d = switch_cap_diameter); } } } module assembly() { color([0.82, 0.84, 0.88, 1]) body(); if ($preview && show_references) { reference_board(); reference_top_feature(); reference_battery_and_switch(); } // Printable lid is mirrored into its installed orientation and exploded up. color([0.92, 0.92, 0.95, 0.88]) translate([0, case_width, body_height + lid_thickness + exploded_view]) rotate([180, 0, 0]) lid(); } module coupon_hole(x, y, diameter, depth) { translate([x, y, -eps]) cylinder(h = depth + 2 * eps, d = diameter); } module insert_coupon() { spacing = 15; coupon_length = spacing * len(coupon_steps) + 8; coupon_width = 32; coupon_height = max( m25_insert_length + insert_melt_relief + 1.2, tripod_insert_length + insert_melt_relief + 1.2 ); difference() { rounded_box([coupon_length, coupon_width, coupon_height], 3); for (i = [0 : len(coupon_steps) - 1]) { x = 7 + i * spacing; coupon_hole( x, 8, m25_insert_hole_diameter + coupon_steps[i], m25_insert_length + insert_melt_relief ); coupon_hole( x, 23, tripod_insert_hole_diameter + coupon_steps[i], tripod_insert_length + insert_melt_relief ); } } } module switch_coupon() { spacing = 18; coupon_length = spacing * len(coupon_steps) + 6; coupon_width = 24; coupon_height = wall_thickness; difference() { rounded_box([coupon_length, coupon_width, coupon_height], 3); for (i = [0 : len(coupon_steps) - 1]) translate([6 + i * spacing, coupon_width / 2, -eps]) cylinder( h = coupon_height + 2 * eps, d = switch_hole_diameter + coupon_steps[i] ); } } module fit_coupon() { segment_length = 34; base_width = 16; base_height = 6; cavity_size = [segment_length - 8, base_width - 8]; male_size = [ cavity_size[0] - 2 * fit_clearance, cavity_size[1] - 2 * fit_clearance ]; difference() { rounded_box([segment_length, base_width, base_height], 2); translate([4, 4, 2]) cube([segment_length - 8, base_width - 8, base_height]); } translate([segment_length + 5, 0, 0]) union() { cube([segment_length, base_width, lid_thickness]); translate([ 4 + fit_clearance, 4 + fit_clearance, lid_thickness - eps ]) linear_extrude(height = lid_lip_depth + eps) difference() { rounded_rect_2d(male_size, 1); translate([lid_lip_thickness, lid_lip_thickness]) rounded_rect_2d([ male_size[0] - 2 * lid_lip_thickness, male_size[1] - 2 * lid_lip_thickness ], 0.5); } } } validate_cutout_spacing(); if (part == "assembly") assembly(); else if (part == "body") body(); else if (part == "lid") lid(); else if (part == "buttons") buttons(); else if (part == "insert_coupon") insert_coupon(); else if (part == "switch_coupon") switch_coupon(); else if (part == "fit_coupon") fit_coupon(); else assert(false, str("Unknown part: ", part));