///|
/// 21-breadboard-rail-clips
///
/// A working example that generates a STEP model for: Clips that hold
/// breadboards to rails, generated for multiple breadboard sizes.
///
/// Usage (stdout):
/// moon run --target native examples/21-breadboard-rail-clips -- --width 54.5 > clip.step
async fn main {
let base_args_spec = {
"width": @cli.opt_double(54.5, help="Breadboard width (mm)", min=10.0),
"height": @cli.opt_double(10.0, help="Clip side height (mm)", min=2.0),
"armThickness": @cli.opt_double(
2.5,
help="Thickness of the clip arms (mm)",
min=1.0,
),
"snapClearance": @cli.opt_double(
0.2,
help="Clearance for the snap fit (mm)",
),
"hookDepth": @cli.opt_double(
2.0,
help="Depth of the retention hook (mm)",
min=0.5,
),
"seatDiameter": @cli.opt_double(
4.0,
help="Diameter of the rail mounting seat (mm)",
min=0.0,
),
"seatDepth": @cli.opt_double(
2.0,
help="Depth of the rail mounting seat (mm)",
min=0.0,
),
}
let args_spec = @cli.with_translate(@common.with_output_args(base_args_spec))
guard @cli.parse(
args_spec,
prog="21-breadboard-rail-clips",
description="Generate a breadboard rail clip STEP model.",
)
is Some(args) else {
return
}
let width = args.double("width")
let height = args.double("height")
let arm_thickness = args.double("armThickness")
let snap_clearance = args.double("snapClearance")
let hook_depth = args.double("hookDepth")
let seat_diameter = args.double("seatDiameter")
let seat_depth = args.double("seatDepth")
let output = @common.output_paths(args)
let (tx, ty, tz) = match args.translate_xyz() {
Some(v) => v
None => {
@cli.eprintln("error: invalid translation")
return
}
}
let nodes = breadboard_rail_clip(
width, height, arm_thickness, snap_clearance, hook_depth, seat_diameter, seat_depth,
)
let mut design = @cad.Design::new(
name="breadboard-rail-clip",
documentation=args.documentation(),
)
for node in nodes {
design = design.add(node.translate(x=tx, y=ty, z=tz))
}
let rotated = design.rotate_x(-90.0)
@common.write_output(rotated, output)
}
///|
fn breadboard_rail_clip(
breadboard_width : Double,
clip_height : Double,
arm_thickness : Double,
snap_clearance : Double,
hook_depth : Double,
rail_seat_diameter : Double,
rail_seat_depth : Double,
) -> Array[@cad.SceneNode] {
let hw = breadboard_width / 2.0
// Profile in XY plane (will be rotated to XZ)
// Inside is at Y in [0, clip_height]
// Bottom plate is at Y in [-arm_thickness, 0]
let profile = [
(-hw - arm_thickness, clip_height + arm_thickness),
(-hw - arm_thickness, -arm_thickness),
(hw + arm_thickness, -arm_thickness),
(hw + arm_thickness, clip_height + arm_thickness),
(hw + arm_thickness - hook_depth, clip_height + arm_thickness),
(hw + arm_thickness - hook_depth, clip_height),
(hw + snap_clearance, clip_height),
(hw + snap_clearance, 0.0),
(-hw - snap_clearance, 0.0),
(-hw - snap_clearance, clip_height),
(-hw - arm_thickness + hook_depth, clip_height),
(-hw - arm_thickness + hook_depth, clip_height + arm_thickness),
]
let clip_length = 15.0
let main_clip = @cad.ExtrudedProfile::new(profile, clip_length)
.with_name("main-clip")
.with_color(@cad.Rgb::cyan())
.rotate_x(90.0)
.translate(y=clip_length / 2.0)
let nodes = [main_clip]
if rail_seat_diameter > 0.0 && rail_seat_depth > 0.0 {
// Two mounting pegs for stability
let seat_dist = clip_length / 2.0 - rail_seat_diameter
let seat_locs = [(-seat_dist, 0.0), (seat_dist, 0.0)]
for i, loc in seat_locs {
let (sy, _) = loc // Centered along World Y (clip length)
let seat = @cad.Cylinder::new(rail_seat_diameter, rail_seat_depth)
.with_name("rail-seat-\{i}")
.with_color(@cad.Rgb::blue())
.translate(x=0.0, y=sy, z=-arm_thickness - rail_seat_depth)
nodes.push(seat)
}
}
nodes
}