Files
copy_stand/copy_stand.scad
T
2026-08-27 07:31:30 -07:00

641 lines
23 KiB
OpenSCAD

/*
* Parametric copy-stand base for a Bambu Lab X2D.
* Service coordinates: X left-to-right, Y rear-to-front, Z bottom-to-top.
* The grid origin is at plate Y=datum_y. The socket flange's straight front
* edge is 0.5 inch forward of that origin, while the rail front face is at 0.
*/
/* [Output] */
part = "assembly"; // [assembly,base,base_black,base_segment,base_black_segment,base_accent,socket,coupon,coupon_black,coupon_key,coupon_accent]
segment_index = 0; // [0:1:5]
show_references = true;
/* [Base] */
plate_width = 203.2;
plate_depth = 248;
plate_height = 15;
corner_radius = 12.7;
datum_y = 26;
top_skin = 3.2;
perimeter_wall = 3.2;
rib_width = 2.4;
/* [Grid inlay] */
grid_pitch = 25.4;
grid_columns = 8;
grid_rows = 8;
grid_line_width = 0.8;
inlay_depth = 0.6;
inlay_clearance = 0.08;
/* [McMaster 97171A240 inserts] */
insert_pilot_diameter = 8.1;
insert_body_length = 9.525;
insert_flange_diameter = 12.9;
insert_flange_depth = 1.35;
screw_clearance_diameter = 6.8;
insert_boss_diameter = 20;
/* [Feet] */
foot_pad_diameter = 19;
foot_pad_height = 8.5;
foot_stud_length = 17.8;
foot_nut_diameter = 24.4;
foot_nut_height = 6;
/* [Removable 2020 socket] */
rail_size = 20;
rail_clearance = 0.05;
socket_capture = 50;
socket_front_y = grid_pitch / 2;
socket_rear_y = -datum_y;
socket_flange_size = [110, socket_front_y - socket_rear_y, 10];
socket_rail_front_y = 0;
socket_key_axis_y = -0.05; // Fixed to the existing base locator slot.
socket_key_bottom_clearance = 0.2;
socket_key_size = [52, 22, plate_height - socket_key_bottom_clearance];
socket_key_clearance = 0.25;
socket_side_wall = 6;
socket_end_wall = 2.7;
socket_mount_x = 45;
socket_mount_ys = [-16, 3];
socket_mount_tool_diameter = 19;
socket_gusset_outer_x = 35;
socket_gusset_y_span = [-23, 10];
socket_gusset_height = 32;
rail_screw_diameter = 5.5;
rail_screw_head_diameter = 9.5;
rail_screw_head_depth = 3;
rail_tool_diameter = 14;
rail_screw_heights = [15, 40];
/* [Calibration] */
coupon_insert_diameters = [7.70, 7.85, 8.00, 8.15, 8.30];
coupon_rail_gaps = [20.0, 20.1, 20.2, 20.3];
$fn = $preview ? 36 : 72;
eps = 0.02;
assert(plate_width <= 256 && plate_depth <= 256,
"Base exceeds the X2D main-nozzle build area");
assert(plate_height >= 13.462,
"Insert bosses violate McMaster's 0.530-inch minimum material thickness");
assert(top_skin > inlay_depth,
"Top skin must be thicker than the inlay");
assert(grid_columns == 8 && grid_rows == 8,
"This release's shared front-foot geometry expects an 8 x 8 grid");
assert(socket_key_size[0] / 2 + socket_key_clearance
< socket_mount_x - insert_boss_diameter / 2,
"Socket key slot overlaps a full-depth mounting-insert boss");
assert(socket_gusset_outer_x
< socket_mount_x - socket_mount_tool_diameter / 2,
"Socket gusset enters a flange-screw tool-access envelope");
assert(socket_front_y == grid_pitch / 2,
"Socket flange reference face must remain at the half-inch grid coordinate");
assert(socket_rail_axis_y() + (rail_size + 2 * rail_clearance) / 2
== socket_rail_front_y,
"Rail front face must remain at the grid origin");
assert(socket_rear_y == -datum_y,
"Socket flange must reach the rear edge of the base");
assert(socket_mount_x + socket_mount_tool_diameter / 2
<= socket_flange_size[0] / 2,
"Socket washer envelope exceeds the flange width");
assert(socket_mount_ys[0] - socket_mount_tool_diameter / 2
>= socket_rear_y
&& socket_mount_ys[len(socket_mount_ys) - 1]
+ socket_mount_tool_diameter / 2 <= socket_front_y,
"Socket washer envelope exceeds the flange depth");
assert(socket_key_axis_y + socket_key_size[1] / 2
+ socket_key_clearance
< grid_pitch - insert_boss_diameter / 2,
"Socket key slot overlaps the first grid-row insert bosses");
assert(socket_mount_ys[len(socket_mount_ys) - 1]
+ insert_boss_diameter / 2
< grid_pitch - insert_boss_diameter / 2,
"Socket mounting bosses overlap the first grid-row bosses");
assert(socket_key_size[2] == plate_height - socket_key_bottom_clearance,
"Socket tongue must leave 0.2 mm clearance above the base underside");
grid_xs = [for (i = [0 : grid_columns - 1])
plate_width / 2 + (i - (grid_columns - 1) / 2) * grid_pitch];
grid_ys = [for (i = [1 : grid_rows]) datum_y + i * grid_pitch];
rear_foot_ys = corner_radius;
socket_hole_xs = [plate_width / 2 - socket_mount_x,
plate_width / 2 + socket_mount_x];
socket_hole_ys = [for (y = socket_mount_ys) datum_y + y];
all_insert_points = concat(
[for (x = grid_xs, y = grid_ys) [x, y]],
[for (x = [grid_xs[0], grid_xs[len(grid_xs) - 1]]) [x, rear_foot_ys]],
[for (x = socket_hole_xs, y = socket_hole_ys) [x, y]]
);
module rounded_rect_2d(size, radius) {
offset(r = radius)
square([size[0] - 2 * radius, size[1] - 2 * radius], center = true);
}
module rounded_prism(size, radius) {
linear_extrude(height = size[2])
rounded_rect_2d([size[0], size[1]], radius);
}
module plate_outline_2d(inset = 0) {
translate([plate_width / 2, plate_depth / 2])
offset(delta = -inset)
rounded_rect_2d([plate_width, plate_depth], corner_radius);
}
module insert_boss_at(x, y) {
translate([x, y, 0]) cylinder(h = plate_height, d = insert_boss_diameter);
}
module insert_positive_bosses() {
for (p = all_insert_points) insert_boss_at(p[0], p[1]);
}
module underside_structure_2d() {
intersection() {
plate_outline_2d();
union() {
difference() {
plate_outline_2d();
plate_outline_2d(perimeter_wall);
}
for (x = grid_xs)
translate([x - rib_width / 2, corner_radius, 0])
square([rib_width, plate_depth - 2 * corner_radius]);
for (y = concat([datum_y], grid_ys))
translate([corner_radius, y - rib_width / 2, 0])
square([plate_width - 2 * corner_radius, rib_width]);
translate([corner_radius, rear_foot_ys - rib_width / 2, 0])
square([plate_width - 2 * corner_radius, rib_width]);
for (x = socket_hole_xs)
translate([x - rib_width / 2, corner_radius, 0])
square([rib_width, datum_y - corner_radius]);
for (p = all_insert_points)
translate(p) circle(d = insert_boss_diameter);
}
}
}
module base_structure_positive() {
union() {
linear_extrude(height = plate_height - top_skin)
underside_structure_2d();
translate([0, 0, plate_height - top_skin])
linear_extrude(height = top_skin) plate_outline_2d();
}
}
module one_insert_cutout(x, y, pilot = insert_pilot_diameter) {
translate([x, y, -eps])
cylinder(h = insert_flange_depth + eps, d = insert_flange_diameter);
translate([x, y, insert_flange_depth - eps])
cylinder(h = insert_body_length + 2 * eps, d = pilot);
translate([x, y, insert_flange_depth + insert_body_length - eps])
cylinder(
h = plate_height - insert_flange_depth - insert_body_length + 2 * eps,
d = screw_clearance_diameter
);
}
module all_insert_cutouts() {
union() {
for (p = all_insert_points)
translate([p[0], p[1], -eps])
cylinder(h = insert_flange_depth + eps,
d = insert_flange_diameter);
}
union() {
for (p = all_insert_points)
translate([p[0], p[1], insert_flange_depth - eps])
cylinder(h = insert_body_length + 2 * eps,
d = insert_pilot_diameter);
}
union() {
for (p = all_insert_points)
translate([p[0], p[1],
insert_flange_depth + insert_body_length - eps])
cylinder(
h = plate_height - insert_flange_depth
- insert_body_length + 2 * eps,
d = screw_clearance_diameter
);
}
}
module insert_pattern_2d(diameter) {
for (p = all_insert_points) translate(p) circle(d = diameter);
}
module socket_key_2d(clearance = 0) {
translate([plate_width / 2, datum_y + socket_key_axis_y])
rounded_rect_2d(
[socket_key_size[0] + 2 * clearance,
socket_key_size[1] + 2 * clearance],
2 + clearance
);
}
module grid_artwork_2d(clearance = 0) {
intersection() {
plate_outline_2d();
offset(delta = clearance)
union() {
for (x = grid_xs)
translate([x - grid_line_width / 2, datum_y])
square([grid_line_width, plate_depth - datum_y]);
for (y = concat([datum_y], grid_ys))
translate([0, y - grid_line_width / 2])
square([plate_width, grid_line_width]);
}
}
}
module accent_service() {
translate([0, 0, plate_height - inlay_depth])
linear_extrude(height = inlay_depth)
difference() {
grid_artwork_2d();
insert_pattern_2d(screw_clearance_diameter);
socket_key_2d(socket_key_clearance);
}
}
module base_layer_2d(outline = false, hole_diameter, artwork = false) {
difference() {
if (outline) plate_outline_2d();
else underside_structure_2d();
insert_pattern_2d(hole_diameter);
// The locator is a through-slot so no unsupported bridge is printed
// against the textured build plate.
socket_key_2d(socket_key_clearance);
if (artwork) grid_artwork_2d(inlay_clearance);
}
}
module base_segment_service(index, with_artwork = false) {
underside_top = plate_height - top_skin;
body_top = insert_flange_depth + insert_body_length;
artwork_bottom = plate_height - inlay_depth;
top_split = (underside_top + artwork_bottom) / 2;
if (index == 0)
linear_extrude(height = insert_flange_depth)
base_layer_2d(false, insert_flange_diameter);
else if (index == 1)
translate([0, 0, insert_flange_depth])
linear_extrude(height = insert_body_length)
base_layer_2d(false, insert_pilot_diameter);
else if (index == 2)
translate([0, 0, body_top])
linear_extrude(height = underside_top - body_top)
base_layer_2d(false, screw_clearance_diameter);
else if (index == 3)
translate([0, 0, underside_top])
linear_extrude(height = top_split - underside_top)
base_layer_2d(true, screw_clearance_diameter);
else if (index == 4)
translate([0, 0, top_split])
linear_extrude(height = artwork_bottom - top_split)
base_layer_2d(true, screw_clearance_diameter);
else if (index == 5)
translate([0, 0, artwork_bottom])
linear_extrude(height = inlay_depth)
base_layer_2d(true, screw_clearance_diameter, with_artwork);
else assert(false, str("Unknown base segment: ", index));
}
module base_layered_service(with_artwork = false) {
for (i = [0 : 5]) base_segment_service(i, with_artwork);
}
module base_black_service() {
base_layered_service(true);
}
module base_complete_service() {
base_layered_service(false);
}
module print_orient_base() {
translate([0, plate_depth, plate_height])
rotate([180, 0, 0]) children();
}
module socket_flange_2d() {
translate([0, (socket_front_y + socket_rear_y) / 2])
rounded_rect_2d([socket_flange_size[0], socket_flange_size[1]], 3);
}
function socket_outer_y() = rail_size + 2 * rail_clearance
+ 2 * socket_end_wall;
function socket_rail_axis_y() = socket_rail_front_y
- (rail_size + 2 * rail_clearance) / 2;
module socket_key_local_2d() {
translate([0, socket_key_axis_y])
rounded_rect_2d([socket_key_size[0], socket_key_size[1]], 2);
}
module socket_gusset(side = 1) {
cavity = rail_size + 2 * rail_clearance;
collar_half_x = (cavity + 2 * socket_side_wall) / 2;
collar_y_min = socket_rail_axis_y() - socket_outer_y() / 2;
collar_y_depth = socket_outer_y();
near_x = side > 0 ? collar_half_x : -collar_half_x - 2;
outer_x = side > 0 ? socket_gusset_outer_x - 2
: -socket_gusset_outer_x;
gusset_depth = socket_gusset_y_span[1] - socket_gusset_y_span[0];
// The full-height attachment follows the collar's front and rear edges,
// while the low outer footprint retains the proven flange support area.
// Tool-access bores below remove material around both fastener families.
hull() {
translate([near_x, collar_y_min, socket_flange_size[2]])
cube([2, collar_y_depth, socket_gusset_height]);
translate([outer_x, socket_gusset_y_span[0], socket_flange_size[2]])
cube([2, gusset_depth, 2]);
}
}
module socket_positive() {
cavity = rail_size + 2 * rail_clearance;
outer_x = cavity + 2 * socket_side_wall;
outer_y = socket_outer_y();
translate([0, 0, -socket_key_size[2]])
linear_extrude(height = socket_key_size[2] + eps)
socket_key_local_2d();
linear_extrude(height = socket_flange_size[2]) socket_flange_2d();
translate([-outer_x / 2, socket_rail_axis_y() - outer_y / 2,
socket_flange_size[2]])
cube([outer_x, outer_y, socket_capture]);
socket_gusset(-1);
socket_gusset(1);
}
module socket_cutouts() {
cavity = rail_size + 2 * rail_clearance;
collar_half_x = (cavity + 2 * socket_side_wall) / 2;
translate([-cavity / 2, socket_rail_axis_y() - cavity / 2,
socket_flange_size[2]])
cube([cavity, cavity, socket_capture + eps]);
// Four base mounting holes with clear vertical driver access.
for (x = [-socket_mount_x, socket_mount_x], y = socket_mount_ys) {
translate([x, y, -socket_key_size[2] - eps])
cylinder(h = socket_flange_size[2] + socket_key_size[2] + 2 * eps,
d = screw_clearance_diameter);
translate([x, y, socket_flange_size[2]])
cylinder(h = socket_gusset_height + eps,
d = socket_mount_tool_diameter);
}
// Two screw levels through both side walls create four M5 clamp points.
for (h = rail_screw_heights) {
translate([-socket_flange_size[0] / 2, socket_rail_axis_y(),
socket_flange_size[2] + h])
rotate([0, 90, 0])
cylinder(h = socket_flange_size[0], d = rail_screw_diameter);
translate([-collar_half_x - eps, socket_rail_axis_y(),
socket_flange_size[2] + h])
rotate([0, 90, 0])
cylinder(h = rail_screw_head_depth + eps,
d = rail_screw_head_diameter);
translate([collar_half_x - rail_screw_head_depth,
socket_rail_axis_y(),
socket_flange_size[2] + h])
rotate([0, 90, 0])
cylinder(h = rail_screw_head_depth + eps,
d = rail_screw_head_diameter);
// Driver tunnels exist only outside the collar, preserving the side
// wall while allowing a socket driver through each buttress.
translate([-socket_gusset_outer_x - eps, socket_rail_axis_y(),
socket_flange_size[2] + h])
rotate([0, 90, 0])
cylinder(h = socket_gusset_outer_x - collar_half_x + 2 * eps,
d = rail_tool_diameter);
translate([collar_half_x - eps, socket_rail_axis_y(),
socket_flange_size[2] + h])
rotate([0, 90, 0])
cylinder(h = socket_gusset_outer_x - collar_half_x + 2 * eps,
d = rail_tool_diameter);
}
}
module socket_service() {
difference() {
socket_positive();
socket_cutouts();
}
}
module socket_print() {
translate([socket_flange_size[0] / 2, -socket_rear_y,
socket_key_size[2]]) socket_service();
}
module reference_rail() {
color([0.08, 0.08, 0.09, 0.8])
translate([plate_width / 2 - rail_size / 2,
datum_y + socket_rail_axis_y() - rail_size / 2,
plate_height + socket_flange_size[2]])
cube([rail_size, rail_size, 400]);
}
module reference_feet() {
foot_points = concat(
[[grid_xs[0], rear_foot_ys],
[grid_xs[len(grid_xs)-1], rear_foot_ys]],
[[grid_xs[0], grid_ys[len(grid_ys)-1]],
[grid_xs[len(grid_xs)-1], grid_ys[len(grid_ys)-1]]]
);
for (p = foot_points) {
color([0.05, 0.05, 0.05, 0.8]) {
translate([p[0], p[1], -foot_nut_height])
cylinder(h = foot_nut_height, d = foot_nut_diameter);
translate([p[0], p[1], -foot_nut_height - foot_pad_height])
cylinder(h = foot_pad_height, d = foot_pad_diameter);
}
}
}
// Two-piece calibration coupon, modeled directly in print orientation.
coupon_size = [165, 110, 3.2];
coupon_boss_height = 15;
coupon_rail_xs = [20, 60, 100, 140];
coupon_rail_y = 23;
coupon_fit_center = [82.5, 90];
coupon_fit_outer = [64, 34];
coupon_fit_height = 6;
coupon_key_depth = 6;
coupon_key_center = [200, 90];
coupon_key_cap = [56, 26, 2];
coupon_key_handle = [40, 6, 8];
assert(coupon_fit_height == coupon_key_depth,
"Coupon socket and tongue gauges must have matching depths");
assert(len(coupon_rail_gaps) == len(coupon_rail_xs),
"Each rail gauge must have one layout position");
assert(len([for (gap = coupon_rail_gaps)
if (gap == rail_size + 2 * rail_clearance) gap]) == 1,
"Production rail cavity must appear once in the coupon gauges");
assert(coupon_rail_xs[0]
- (coupon_rail_gaps[len(coupon_rail_gaps) - 1]
+ 2 * socket_side_wall) / 2 >= 0
&& coupon_rail_xs[len(coupon_rail_xs) - 1]
+ (coupon_rail_gaps[len(coupon_rail_gaps) - 1]
+ 2 * socket_side_wall) / 2 <= coupon_size[0],
"Rail gauges exceed the main coupon width");
assert(coupon_fit_center[0] - coupon_fit_outer[0] / 2 >= 0
&& coupon_fit_center[0] + coupon_fit_outer[0] / 2 <= coupon_size[0]
&& coupon_fit_center[1] - coupon_fit_outer[1] / 2 >= 0
&& coupon_fit_center[1] + coupon_fit_outer[1] / 2 <= coupon_size[1],
"Coupon fit socket exceeds the main coupon outline");
assert(coupon_key_center[0] - coupon_key_cap[0] / 2 > coupon_size[0],
"Coupon tongue must remain a physically separate print");
assert(coupon_key_center[0] + coupon_key_cap[0] / 2 <= 256
&& coupon_key_center[1] + coupon_key_cap[1] / 2 <= 256,
"Coupon and separate tongue exceed the X2D build area");
module coupon_outline_2d() {
translate([coupon_size[0] / 2, coupon_size[1] / 2])
rounded_rect_2d([coupon_size[0], coupon_size[1]], 4);
}
module coupon_accent_2d(clearance = 0) {
offset(delta = clearance)
translate([8, 6]) square([75, grid_line_width]);
}
module coupon_positive() {
linear_extrude(height = coupon_size[2]) coupon_outline_2d();
for (i = [0 : len(coupon_insert_diameters) - 1])
translate([14 + i * 24, 56, coupon_size[2]])
cylinder(h = coupon_boss_height - coupon_size[2], d = insert_boss_diameter);
// Four production-style closed collars, left-to-right 20.0-20.3 mm.
for (i = [0 : len(coupon_rail_gaps) - 1]) {
gap = coupon_rail_gaps[i];
outer_x = gap + 2 * socket_side_wall;
outer_y = gap + 2 * socket_end_wall;
translate([coupon_rail_xs[i], coupon_rail_y,
coupon_size[2] - eps])
difference() {
translate([-outer_x / 2, -outer_y / 2, 0])
cube([outer_x, outer_y, socket_capture + eps]);
translate([-gap / 2, -gap / 2, -eps])
cube([gap, gap, socket_capture + 3 * eps]);
}
}
// Full-depth test socket surrounding the production-size through-slot.
translate([coupon_fit_center[0], coupon_fit_center[1],
coupon_size[2] - eps])
rounded_prism(
[coupon_fit_outer[0], coupon_fit_outer[1],
coupon_fit_height - coupon_size[2] + eps],
3
);
}
module coupon_common_cutouts() {
for (i = [0 : len(coupon_insert_diameters) - 1]) {
x = 14 + i * 24;
pilot = coupon_insert_diameters[i];
translate([x, 56, coupon_boss_height - insert_flange_depth])
cylinder(h = insert_flange_depth + eps, d = insert_flange_diameter);
translate([x, 56, coupon_boss_height - insert_flange_depth - insert_body_length])
cylinder(h = insert_body_length + eps, d = pilot);
translate([x, 56, -eps])
cylinder(h = coupon_boss_height - insert_flange_depth
- insert_body_length + 2 * eps,
d = screw_clearance_diameter);
}
translate([coupon_fit_center[0], coupon_fit_center[1], -eps])
rounded_prism(
[socket_key_size[0] + 2 * socket_key_clearance,
socket_key_size[1] + 2 * socket_key_clearance,
coupon_fit_height + 2 * eps],
2 + socket_key_clearance
);
}
module coupon_key() {
// Separate full-size tongue. A 45-degree stop shoulder remains
// support-free, and the vertical rib provides a grip for removal.
translate([coupon_key_center[0], coupon_key_center[1], 0]) {
rounded_prism(
[socket_key_size[0], socket_key_size[1], coupon_key_depth], 2);
hull() {
translate([0, 0, coupon_key_depth - eps])
rounded_prism(
[socket_key_size[0], socket_key_size[1], eps], 2);
translate([0, 0,
coupon_key_depth + coupon_key_cap[2] - eps])
rounded_prism(
[coupon_key_cap[0], coupon_key_cap[1], eps], 3);
}
translate([0, 0, coupon_key_depth + coupon_key_cap[2] - eps])
rounded_prism(
[coupon_key_handle[0], coupon_key_handle[1],
coupon_key_handle[2] + eps],
2
);
}
}
module coupon_black() {
difference() {
coupon_positive();
coupon_common_cutouts();
translate([0, 0, -eps])
linear_extrude(height = inlay_depth + 2 * eps)
coupon_accent_2d(inlay_clearance);
}
}
module coupon_accent() {
linear_extrude(height = inlay_depth) coupon_accent_2d();
}
module coupon_complete() {
union() {
difference() {
coupon_positive();
coupon_common_cutouts();
}
coupon_key();
}
}
module assembly() {
color([0.035, 0.035, 0.04]) base_black_service();
color([0.05, 0.25, 0.8]) accent_service();
color([0.035, 0.035, 0.04])
translate([plate_width / 2, datum_y, plate_height]) socket_service();
if (show_references) {
reference_rail();
reference_feet();
}
}
if (part == "assembly") assembly();
else if (part == "base") print_orient_base() base_complete_service();
else if (part == "base_black") print_orient_base() base_black_service();
else if (part == "base_segment")
print_orient_base() base_segment_service(segment_index, false);
else if (part == "base_black_segment")
print_orient_base() base_segment_service(segment_index, true);
else if (part == "base_accent") print_orient_base() accent_service();
else if (part == "socket") socket_print();
else if (part == "coupon") coupon_complete();
else if (part == "coupon_black") coupon_black();
else if (part == "coupon_key") coupon_key();
else if (part == "coupon_accent") coupon_accent();
else assert(false, str("Unknown part: ", part));