commit db51f482894be0250bea060952bf58a160082b5c Author: Sam Hadow Date: Sun Feb 22 11:33:39 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1567411 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.stl diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/backplate.scad b/backplate.scad new file mode 100644 index 0000000..4147114 --- /dev/null +++ b/backplate.scad @@ -0,0 +1,99 @@ +// --- Toggle which part to render --- +render_part = "full"; + +// --- Parameters --- +columns = 14; +rows = 5; +u_size = 19.05; + +switch_outer_width = 14.2; +switch_inner_width = 13.2; +switch_total_height = 14.2; +notch_height = 4.0; + +plate_thickness = 1.5; +margin = 5.0; + +// --- Calculated Plate Dimensions --- +plate_width = (columns - 1) * u_size + switch_outer_width + (margin * 2); +plate_height = (rows - 1) * u_size + switch_total_height + (margin * 2); +split_point = plate_width / 2; + +// --- Modules --- +module switch_cutout() { + union() { + cube([switch_inner_width, switch_total_height, plate_thickness + 2], center=true); + translate([0, (switch_total_height - (switch_total_height-notch_height)/2)/2, 0]) + cube([switch_outer_width, (switch_total_height-notch_height)/2, plate_thickness + 2], center=true); + translate([0, -(switch_total_height - (switch_total_height-notch_height)/2)/2, 0]) + cube([switch_outer_width, (switch_total_height-notch_height)/2, plate_thickness + 2], center=true); + } +} + +module stabilizer_cutout(wire_len_u) { + dist = (wire_len_u == 6.25) ? 100 : 23.85; + union() { + switch_cutout(); + for(m = [-1, 1]) { + translate([m * dist/2, 0, 0]) { + cube([3.3, 14.0, plate_thickness + 2], center=true); + translate([m * 0.5, 0, 0]) + cube([4.3, 8.0, plate_thickness + 2], center=true); + } + } + } +} + +module whole_plate() { + difference() { + cube([plate_width, plate_height, plate_thickness]); + + translate([margin + switch_outer_width/2, margin + switch_total_height/2, plate_thickness/2]) { + for (r = [0 : rows - 1]) { + for (c = [0 : columns - 1]) { + translate([c * u_size, r * u_size, 0]) { + + // --- ROW 0: BOTTOM ROW --- + if (r == 0) { + if (c < 3) { + switch_cutout(); + } + else if (c == 3) { + translate([u_size * 3, 0, 0]) stabilizer_cutout(6.25); + } + else if (c >= 10) { + switch_cutout(); + } + } + + // --- ROW 2: THIRD ROW (ENTER) --- + else if (r == 2) { + if (c < 12) { + switch_cutout(); + } else if (c == 12) { + translate([u_size * 0.5, 0, 0]) stabilizer_cutout(2); + } + } + + // --- ALL OTHER ROWS --- + else { + switch_cutout(); + } + } + } + } + } + } +} + +// --- Render --- +if (render_part == "left") { + intersection() { whole_plate(); cube([split_point, plate_height, plate_thickness + 2]); } +} else if (render_part == "right") { + translate([-split_point, 0, 0]) intersection() { + whole_plate(); + translate([split_point, 0, -1]) cube([split_point, plate_height, plate_thickness + 2]); + } +} else { + whole_plate(); +} \ No newline at end of file diff --git a/backplate_numpad.scad b/backplate_numpad.scad new file mode 100644 index 0000000..15d78e6 --- /dev/null +++ b/backplate_numpad.scad @@ -0,0 +1,82 @@ +// --- Parameters --- +columns = 4; +rows = 6; +u_size = 19.05; + +switch_outer_width = 14.2; +switch_inner_width = 13.2; +switch_total_height = 14.2; +notch_height = 4.0; + +plate_thickness = 1.5; +margin = 5.0; + +// --- Calculated Plate Dimensions --- +plate_width = (columns - 1) * u_size + switch_outer_width + (margin * 2); +plate_height = (rows - 1) * u_size + switch_total_height + (margin * 2); + +// --- Modules --- +module switch_cutout() { + union() { + cube([switch_inner_width, switch_total_height, plate_thickness + 2], center=true); + translate([0, (switch_total_height - (switch_total_height-notch_height)/2)/2, 0]) + cube([switch_outer_width, (switch_total_height-notch_height)/2, plate_thickness + 2], center=true); + translate([0, -(switch_total_height - (switch_total_height-notch_height)/2)/2, 0]) + cube([switch_outer_width, (switch_total_height-notch_height)/2, plate_thickness + 2], center=true); + } +} + +module stabilizer_cutout(is_vertical=false) { + dist = 23.85; // Standard 2u stabilizer distance + union() { + switch_cutout(); + rotate([0, 0, is_vertical ? 90 : 0]) { + for(m = [-1, 1]) { + translate([m * dist/2, 0, 0]) { + cube([3.3, 14.0, plate_thickness + 2], center=true); + translate([m * 0.5, 0, 0]) + cube([4.3, 8.0, plate_thickness + 2], center=true); + } + } + } + } +} + +module whole_plate() { + difference() { + cube([plate_width, plate_height, plate_thickness]); + + translate([margin + switch_outer_width/2, margin + switch_total_height/2, plate_thickness/2]) { + for (r = [0 : rows - 1]) { + for (c = [0 : columns - 1]) { + translate([c * u_size, r * u_size, 0]) { + + // 1. COLUMN 3: (Enter, Plus, and 2 1U Keys) + if (c == 3) { + if (r == 0 || r == 2) { + // Bottom 2 2U Vertical keys + translate([0, u_size/2, 0]) stabilizer_cutout(true); + } else if (r == 4 || r == 5) { + // Top 2 1U keys + switch_cutout(); + } + } + + // 2. BOTTOM ROW: 2U '0' Key (Cols 0 and 1) + else if (r == 0 && (c == 0 || c == 1)) { + if (c == 0) translate([u_size/2, 0, 0]) stabilizer_cutout(false); + } + + // 3. OTHER 1U KEYS + else { + switch_cutout(); + } + } + } + } + } + } +} + +// --- full render --- +whole_plate(); \ No newline at end of file diff --git a/case.scad b/case.scad new file mode 100644 index 0000000..1b8059e --- /dev/null +++ b/case.scad @@ -0,0 +1,188 @@ +// --- Parameters --- +columns = 14; +rows = 5; +switch_spacing = 19.05; +switch_outer_width = 14.2; +switch_total_height = 14.2; +margin = 5.0; + +plate_w = (columns - 1) * switch_spacing + switch_outer_width + (margin * 2); +plate_h = (rows - 1) * switch_spacing + switch_total_height + (margin * 2); + +// --- Plate --- +plate_thickness = 1.5; +plate_slack = 0.15; +recess_depth = 2.5; + +// --- Case Dimensions --- +case_angle = 5; +wall_thickness = 4.0; +front_height = 18.0; +clearance = 0.25; +floor_thickness = 4.0; +rim_extra_height = 3.0; + +pillar_dia = 3; + +case_w = plate_w + (wall_thickness * 2) + (clearance * 2) - 2; +case_h = ((plate_h - 2) * cos(case_angle)) + (wall_thickness * 2) + (clearance * 2); + +back_height = front_height + (tan(case_angle) * case_h); + +// --- USB-C and Controller Holder --- +usb_w = 12.0; +usb_h = 6.5; +usb_x_offset = 60.0; +usb_z_pos = floor_thickness; + +holder_x = 20.0; +holder_y = 35.0; +holder_height = 5.0; +holder_wall = 2.0; + +// --- Splitting --- +joint_clearance = 0.05; +tab_size = 10; + +// --- Clips --- +clip_width = 10.0; +clip_depth = 1.5; +clip_height = 1.5; + +module controller_holder() { + x_pos = usb_x_offset + (usb_w/2) - (holder_x/2) - holder_wall; + y_inner_wall = case_h - wall_thickness; + y_pos = y_inner_wall - holder_y - holder_wall; + + translate([x_pos, y_pos, floor_thickness]) { + difference() { + cube([holder_x + (holder_wall * 2), holder_y + holder_wall, holder_height]); + translate([holder_wall, holder_wall, -0.1]) + cube([holder_x, holder_y + 0.1, holder_height + 0.2]); + } + } +} + +module usb_cutout() { + translate([usb_x_offset, case_h - wall_thickness - 1, usb_z_pos]) + cube([usb_w, wall_thickness + 2, usb_h]); +} + +module angled_clip() { + color("red") + rotate([case_angle, 0, 0]) + cube([clip_width, clip_depth, clip_height]); +} + +module support_pillars() { + // Gap indices: x means the gap between column/row x and x+1 + col_gaps = [3, 6, 8, 11]; + row_gaps = [1, 2, 3]; + + // Plate start positions in case coordinates + plate_start_x = (case_w - plate_w)/2 - 2; + plate_start_y = (case_h - (plate_h * cos(case_angle)))/2 - 2 * cos(case_angle); + + intersection() { + union() { + for (c = col_gaps) { + for (r = row_gaps) { + // X is simple: plate offset + margin + (gap index * spacing) + x_pos = plate_start_x + margin + (c * switch_spacing); + + // Y must account for the tilt contraction (cos) + y_pos_flat = margin + (r * switch_spacing); + y_pos = plate_start_y + (y_pos_flat * cos(case_angle)); + + translate([x_pos, y_pos, 0]) + cylinder(d = pillar_dia, h = back_height + 10, $fn = 32); + } + } + } + + // Clipping volume: Vertical space from floor up to the underside of the tilted plate + // This ensures the pillars are vertical but have an angled top surface + translate([0, plate_start_y, front_height - recess_depth]) + rotate([case_angle, 0, 0]) + translate([-50, 0, -500]) // Big volume below the plate + cube([case_w + 100, plate_h, 500]); + } +} + +module case_full_geometry() { + clip_z_offset = plate_thickness + plate_slack; + + // Exact dimensions for the recess + recess_w = plate_w + (plate_slack * 2); + recess_h = plate_h + (plate_slack * 2); + + union() { + difference() { + // 1. Shell + polyhedron( + points=[ + [0,0,0], [case_w,0,0], [case_w,case_h,0], [0,case_h,0], + [0,0,front_height + rim_extra_height], [case_w,0,front_height + rim_extra_height], + [0,case_h,back_height + rim_extra_height], [case_w,case_h,back_height + rim_extra_height] + ], + faces=[[0,1,2,3], [4,5,1,0], [7,6,3,2], [5,7,2,1], [6,4,0,3], [4,6,7,5]] + ); + + // 2. Internal Tub + translate([wall_thickness, wall_thickness, floor_thickness]) + cube([case_w - (wall_thickness * 2), case_h - (wall_thickness * 2), back_height + 20]); + + // 3. The Plate Recess (Hardcode removed, now using plate_slack) + translate([(case_w - recess_w)/2, (case_h - (recess_h * cos(case_angle)))/2, front_height - recess_depth]) + rotate([case_angle, 0, 0]) + cube([recess_w, recess_h, 20]); + } + + support_pillars(); + + // 4. Clips + for (i = [0:5]) { + z_front = (front_height - recess_depth) + clip_z_offset; + translate([wall_thickness + (case_w - wall_thickness*2)/7 * (i+1) - clip_width/2, wall_thickness - clip_depth, z_front]) + angled_clip(); + + y_back_wall = case_h - wall_thickness; + z_back_shelf = (front_height - recess_depth) + (tan(case_angle) * (y_back_wall - wall_thickness)); + z_back_clip = z_back_shelf + clip_z_offset; + translate([wall_thickness + (case_w - wall_thickness*2)/7 * (i+1) - clip_width/2, y_back_wall - clip_depth * 0.5, z_back_clip]) + angled_clip(); + } + } +} + +module jigsaw_cutter(gap) { + union() { + translate([case_w/2 + gap, -1, -1]) + cube([case_w, case_h + 2, back_height + 40]); + translate([case_w/2 - tab_size + gap, case_h/2 - 15, -1]) + linear_extrude(back_height + 40) + polygon(points=[[gap,0], [tab_size, -5], [tab_size, 35], [gap, 30]]); + } +} + +// --- Render --- +render_part = "both"; + +if (render_part == "left" || render_part == "both") { + union() { + difference() { + case_full_geometry(); + jigsaw_cutter(-joint_clearance); + usb_cutout(); + } + controller_holder(); + } +} + +if (render_part == "right" || render_part == "both") { + translate([render_part == "both" ? 10 : 0, 0, 0]) + intersection() { + case_full_geometry(); + jigsaw_cutter(joint_clearance); + } +} \ No newline at end of file diff --git a/case_numpad.scad b/case_numpad.scad new file mode 100644 index 0000000..6253a0a --- /dev/null +++ b/case_numpad.scad @@ -0,0 +1,149 @@ +// --- Parameters --- +columns = 4; // Numpad Width +rows = 6; // Numpad Height +switch_spacing = 19.05; +switch_outer_width = 14.2; +switch_total_height = 14.2; +margin = 5.0; + +plate_w = (columns - 1) * switch_spacing + switch_outer_width + (margin * 2); +plate_h = (rows - 1) * switch_spacing + switch_total_height + (margin * 2); + +// --- Plate --- +plate_thickness = 1.5; +plate_slack = 0.15; +recess_depth = 2.5; + +// --- Case Dimensions --- +case_angle = 5; +wall_thickness = 4.0; +front_height = 18.0; +clearance = 0.25; +floor_thickness = 4.0; +rim_extra_height = 3.0; + +pillar_dia = 3; + +case_w = plate_w + (wall_thickness * 2) + (clearance * 2) - 2; +case_h = (plate_h * cos(case_angle)) + (wall_thickness * 2) + (clearance * 2) - 2 * cos(case_angle); + +back_height = front_height + (tan(case_angle) * case_h); + +// --- USB-C and Controller Holder --- +usb_w = 12.0; +usb_h = 6.5; +usb_x_offset = (case_w / 2) - (usb_w / 2); +usb_z_pos = floor_thickness; + +holder_x = 20.0; +holder_y = 30.0; +holder_height = 5.0; +holder_wall = 2.0; + +// --- Clips --- +clip_width = 10.0; +clip_depth = 1.5; +clip_height = 1.5; + +module controller_holder() { + x_pos = (case_w / 2) - (holder_x / 2) - holder_wall; + y_inner_wall = case_h - wall_thickness; + y_pos = y_inner_wall - holder_y - holder_wall; + + translate([x_pos, y_pos, floor_thickness]) { + difference() { + cube([holder_x + (holder_wall * 2), holder_y + holder_wall, holder_height]); + translate([holder_wall, holder_wall, -0.1]) + cube([holder_x, holder_y + 0.1, holder_height + 0.2]); + } + } +} + +module usb_cutout() { + translate([usb_x_offset, case_h - wall_thickness - 1, usb_z_pos]) + cube([usb_w, wall_thickness + 2, usb_h]); +} + +module angled_clip() { + color("red") + rotate([case_angle, 0, 0]) + cube([clip_width, clip_depth, clip_height]); +} + +module support_pillars() { + col_gaps = [1, 3]; + row_gaps = [1, 3, 5]; + + plate_start_x = (case_w - plate_w)/2 - 2; + plate_start_y = (case_h - (plate_h * cos(case_angle)))/2 - 2 * cos(case_angle); + + intersection() { + union() { + for (c = col_gaps) { + for (r = row_gaps) { + x_pos = plate_start_x + margin + (c * switch_spacing); + y_pos_flat = margin + (r * switch_spacing); + y_pos = plate_start_y + (y_pos_flat * cos(case_angle)); + + translate([x_pos, y_pos, 0]) + cylinder(d = pillar_dia, h = back_height + 10, $fn = 32); + } + } + } + translate([0, plate_start_y, front_height - recess_depth]) + rotate([case_angle, 0, 0]) + translate([-50, 0, -500]) + cube([case_w + 100, plate_h, 500]); + } +} + +module case_full_geometry() { + clip_z_offset = plate_thickness + plate_slack; + recess_w = plate_w + (plate_slack * 2); + recess_h = plate_h + (plate_slack * 2); + + union() { + difference() { + // Shell + polyhedron( + points=[ + [0,0,0], [case_w,0,0], [case_w,case_h,0], [0,case_h,0], + [0,0,front_height + rim_extra_height], [case_w,0,front_height + rim_extra_height], + [0,case_h,back_height + rim_extra_height], [case_w,case_h,back_height + rim_extra_height] + ], + faces=[[0,1,2,3], [4,5,1,0], [7,6,3,2], [5,7,2,1], [6,4,0,3], [4,6,7,5]] + ); + + // Internal Tub + translate([wall_thickness, wall_thickness, floor_thickness]) + cube([case_w - (wall_thickness * 2), case_h - (wall_thickness * 2), back_height + 20]); + + // Plate Recess + translate([(case_w - recess_w)/2, (case_h - (recess_h * cos(case_angle)))/2, front_height - recess_depth]) + rotate([case_angle, 0, 0]) + cube([recess_w, recess_h, 20]); + } + + support_pillars(); + + // Clips (Reduced number for smaller case) + for (i = [0:2]) { + z_front = (front_height - recess_depth) + clip_z_offset; + translate([wall_thickness + (case_w - wall_thickness*2)/4 * (i+1) - clip_width/2, wall_thickness - clip_depth, z_front]) + angled_clip(); + + y_back_wall = case_h - wall_thickness; + z_back_shelf = (front_height - recess_depth) + (tan(case_angle) * (y_back_wall - wall_thickness)); + z_back_clip = z_back_shelf + clip_z_offset; + translate([wall_thickness + (case_w - wall_thickness*2)/4 * (i+1) - clip_width/2, y_back_wall - clip_depth * 0.5, z_back_clip]) + angled_clip(); + } + } +} + +// --- Final Render (Single Part) --- +difference() { + case_full_geometry(); + usb_cutout(); +} +controller_holder(); \ No newline at end of file diff --git a/keycaps_numpad.sh b/keycaps_numpad.sh new file mode 100755 index 0000000..505cfbc --- /dev/null +++ b/keycaps_numpad.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +labels=( +"1" "2" "3" "4" "5" "6" "7" "8" "9" "Num" "/" "*" "-" "." "Esc" "F1" "F2" "F3" +) +labels2=( +"0" +) +labels3=( +"+" "Ent" +) + +mkdir -p keycaps_numpad_stl + +echo "Starting STL generation..." + +for label in "${labels[@]}"; do + echo "Generating: $label.stl" + openscad \ + -o "keycaps_numpad_stl/$label.stl" \ + -D "legend_char=\"$label\"" \ + -D "legend_size=5" \ + ./xda_keycap.scad +done + +for label in "${labels2[@]}"; do + echo "Generating: $label.stl" + openscad \ + -o "keycaps_numpad_stl/$label.stl" \ + -D "legend_char=\"$label\"" \ + -D "legend_size=5" \ + -D "u_count_x=2" \ + ./xda_keycap.scad +done + +for label in "${labels3[@]}"; do + echo "Generating: $label.stl" + openscad \ + -o "keycaps_numpad_stl/$label.stl" \ + -D "legend_char=\"$label\"" \ + -D "legend_size=5" \ + -D "u_count_y=2" \ + ./xda_keycap.scad +done + +echo "Done! Check the 'keycaps_numpad_stl' folder." diff --git a/xda_keycap.scad b/xda_keycap.scad new file mode 100644 index 0000000..abcbea9 --- /dev/null +++ b/xda_keycap.scad @@ -0,0 +1,159 @@ +// --- Parameters --- +$fn = 512; + +// Key Dimensions +u_count_x = 1; +u_count_y = 1; +u_pitch = 19.05; + +width_base_1u = 18.2; +width_top_1u = 14.5; +height = 9.0; +corner_radius = 2.5; + +// Engraving Settings +legend_char = "A"; +legend_depth = 0.5; +legend_size = 5; + +// Calculated Dimensions +is_2u = (u_count_x >= 2 || u_count_y >= 2); +w_base = width_base_1u + (u_count_x - 1) * u_pitch; +h_base = width_base_1u + (u_count_y - 1) * u_pitch; +w_top = width_top_1u + (u_count_x - 1) * u_pitch; +h_top = width_top_1u + (u_count_y - 1) * u_pitch; + +// Dish (Only for 1u) +dish_depth = 1; +dish_radius = 45; + +// Stem & Stabilizer Dimensions +stem_h = 5.0; +stem_outer_d = 5.5; +cross_l = 4.1; +cross_w1 = 1.25; +cross_w2 = 1.15; +stab_dist = 23.85; + + // Supports +support_depth = 5; + +// --- Modules --- + +module rounded_rectangle(w, h, z, r) { + hull() { + translate([r, r, 0]) cylinder(r=r, h=z); + translate([w - r, r, 0]) cylinder(r=r, h=z); + translate([r, h - r, 0]) cylinder(r=r, h=z); + translate([w - r, h - r, 0]) cylinder(r=r, h=z); + } +} + +module xda_body() { + difference() { + hull() { + rounded_rectangle(w_base, h_base, 0.1, corner_radius); + translate([(w_base - w_top)/2, (h_base - h_top)/2, height - 0.1]) + rounded_rectangle(w_top, h_top, 0.1, corner_radius); + } + + if (!is_2u) { + translate([w_base/2, h_base/2, height + dish_radius - dish_depth]) + sphere(r=dish_radius); + } + + translate([0,0,-0.5]) + hull() { + translate([1.5, 1.5, 0]) + rounded_rectangle(w_base - 3, h_base - 3, 0.1, corner_radius/2); + translate([(w_base - w_top)/2 + 1.5, (h_base - h_top)/2 + 1.5, height - 2.5]) + rounded_rectangle(w_top - 3, h_top - 3, 0.1, corner_radius/2); + } + } +} + +module single_stem(pos_x, pos_y) { + translate([pos_x, pos_y, 0]) { + intersection() { + cylinder(d=stem_outer_d, h=height - (is_2u ? 1.0 : dish_depth) - 0.5); + difference() { + cylinder(d=stem_outer_d, h=height); + translate([0, 0, -0.1]) { + cube([cross_l, cross_w1, stem_h * 2], center=true); + cube([cross_w2, cross_l, stem_h * 2], center=true); + } + } + } + } +} + +module all_stems() { + center_x = w_base / 2; + center_y = h_base / 2; + single_stem(center_x, center_y); + if (u_count_x >= 2) { + single_stem(center_x - stab_dist/2, center_y); + single_stem(center_x + stab_dist/2, center_y); + } + if (u_count_y >= 2) { + single_stem(center_x, center_y - stab_dist/2); + single_stem(center_x, center_y + stab_dist/2); + } +} + +module side_engraving() { + // Calculate angle based on 1u profile so text sits flush on the slope + side_angle = atan((width_base_1u - width_top_1u) / (2 * height)); + // Offset slightly from the bottom edge + y_offset = (width_base_1u - width_top_1u) / 4; + + translate([w_base/2, y_offset, height/2]) + rotate([90 - side_angle, 0, 0]) + translate([0, 0, -legend_depth + 0.1]) + linear_extrude(height = legend_depth + 0.5) { + text(legend_char, size = legend_size, + halign = "center", valign = "center", + font = "Overpass:style=Bold"); + } +} + + module support_rib() { + rib_thickness = 1.2; + wall_offset = 1.5; + stem_radius = stem_outer_d / 2; + // Inner wall starts at (width_base_1u/2 - wall_offset) + rib_length = (width_base_1u / 2) - wall_offset - stem_radius - 0.1; + translate([width_base_1u/2, width_base_1u/2, 0]) { + translate([stem_radius- 0.1, -rib_thickness/2, support_depth]) + cube([rib_length, rib_thickness, height - dish_depth - support_depth]); + } +} + + +module supports() { + union() { + support_rib(); + rotate([0, 0, 180]) + translate([-width_base_1u, -width_base_1u, 0]) + support_rib(); + rotate([0, 0, 90]) + translate([0, -width_base_1u, 0]) + support_rib(); + rotate([0, 0, 270]) + translate([-width_base_1u, 0, 0]) + support_rib(); + } +} + +// Final Assembly +difference() { + union() { + xda_body(); + all_stems(); + // Ribs only for 1u + if (!is_2u) { + supports(); + } + } + side_engraving(); +} \ No newline at end of file