///|
/// 15-metric-threaded-rod-nut
///
/// A working example that generates a STEP model for a nut-like coupler body
/// with modeled internal threads and correct clearances.
///
/// Usage (stdout):
/// moon run --target native examples/15-metric-threaded-rod-nut -- --boltDiameter 8 --clearance 0.1 > nut-strip.step
async fn main {
let base_args_spec = {
"boltDiameter": @cli.opt_double(
8.0,
help="Nominal bolt diameter (mm)",
min=1.0,
),
"clearance": @cli.opt_double(0.1, help="Base clearance (mm)", min=0.0),
"interference": @cli.opt_double(0.0, help="Base interference (mm)", min=0.0),
"stepCount": @cli.opt_double(5.0, help="Number of test steps", min=1.0),
"clearanceStep": @cli.opt_double(
0.05,
help="Clearance increment per step (mm)",
),
"holeSpacing": @cli.opt_double(
20.0,
help="Spacing between holes (mm)",
min=5.0,
),
"mountThickness": @cli.opt_double(
5.0,
help="Thickness of the base plate (mm)",
min=1.0,
),
"nutHeight": @cli.opt_double(
0.0,
help="Height of the nut (0 for standard)",
min=0.0,
),
}
let args_spec = @cli.with_translate(@common.with_output_args(base_args_spec))
guard @cli.parse(
args_spec,
prog="15-metric-threaded-rod-nut",
description="Generate a metric threaded rod nut test strip.",
)
is Some(args) else {
return
}
let bolt_diameter = args.double("boltDiameter")
let clearance = args.double("clearance")
let interference = args.double("interference")
let step_count = args.double("stepCount").to_int()
let clearance_step = args.double("clearanceStep")
let hole_spacing = args.double("holeSpacing")
let mount_thickness = args.double("mountThickness")
let nut_height_opt = args.double("nutHeight")
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 nut_height = if nut_height_opt > 0.0 {
Some(nut_height_opt)
} else {
None
}
let nodes = metric_nut_test_strip(
bolt_diameter,
clearance,
interference,
step_count,
clearance_step,
hole_spacing,
mount_thickness,
nut_height?,
)
let mut design = @cad.Design::new(
name="metric-threaded-rod-nut",
documentation=args.documentation(),
)
for node in nodes {
design = design.add(node.translate(x=tx, y=ty, z=tz))
}
@common.write_output(design, output)
}
///|
fn metric_nut_test_strip(
bolt_diameter : Double,
base_clearance : Double,
base_interference : Double,
step_count : Int32,
clearance_step : Double,
hole_spacing : Double,
mount_thickness : Double,
nut_height? : Double,
) -> Array[@cad.SceneNode] {
let (s, h) = metric_nut_dimensions(bolt_diameter)
let nut_h = nut_height.unwrap_or(h)
let total_length = Double::from_int(step_count) * hole_spacing
let base_width = s + 10.0
let base = @cad.Cuboid::new(total_length, base_width, mount_thickness)
.with_name("base")
.with_color(@cad.Rgb::cyan())
.translate(x=0.0, y=-base_width / 2.0, z=0.0)
let nodes = [base]
for i in 0.. (Double, Double) {
if bolt_diameter <= 3.0 {
(5.5, 2.4)
} else if bolt_diameter <= 4.0 {
(7.0, 3.2)
} else if bolt_diameter <= 5.0 {
(8.0, 4.0)
} else if bolt_diameter <= 6.0 {
(10.0, 5.0)
} else if bolt_diameter <= 8.0 {
(13.0, 6.5)
} else if bolt_diameter <= 10.0 {
(17.0, 8.0)
} else if bolt_diameter <= 12.0 {
(19.0, 10.0)
} else {
// Heuristic for larger bolts: S approx 1.7 * D, H approx 0.8 * D
(bolt_diameter * 1.7, bolt_diameter * 0.8)
}
}
///|
/// metric_thread_pitch returns the coarse metric pitch for a nominal diameter.
fn metric_thread_pitch(bolt_diameter : Double) -> Double {
if bolt_diameter <= 3.0 {
0.5
} else if bolt_diameter <= 4.0 {
0.7
} else if bolt_diameter <= 5.0 {
0.8
} else if bolt_diameter <= 6.0 {
1.0
} else if bolt_diameter <= 8.0 {
1.25
} else if bolt_diameter <= 10.0 {
1.5
} else if bolt_diameter <= 12.0 {
1.75
} else if bolt_diameter <= 16.0 {
2.0
} else {
bolt_diameter * 0.125
}
}
///|
/// metric_nut returns the hex body plus a helical internal-thread ridge.
///
/// The bore is at the thread root (major diameter). The ridge is a solid swept
/// along a right-hand helix: its crest sits at the minor diameter and its root
/// is embedded `embed` into the bore wall, so the two bodies fuse when printed.
fn metric_nut(
bolt_diameter : Double,
clearance : Double,
height? : Double,
name_prefix~ : String = "nut",
) -> Array[@cad.SceneNode] {
let (s, h) = metric_nut_dimensions(bolt_diameter)
let height = height.unwrap_or(h)
let pitch = metric_thread_pitch(bolt_diameter)
let major_d = bolt_diameter + clearance
// ISO 68-1 basic profile: 60-degree flanks and an internal radial thread
// depth of H1 = 0.54125 * pitch. The ridge is a trapezoid whose crest is
// P/4 wide at the minor diameter and whose root is 7P/8 wide at the major
// diameter, leaving the standard P/8 groove at the root.
let depth = 0.54125 * pitch
let half_depth = depth / 2.0
let crest_half = 0.125 * pitch
let root_half = 0.4375 * pitch
let minor_d = major_d - 2.0 * depth
let d = s / @math.cos(30.0 * @math.PI / 180.0)
let outer = @cad.Profile2D::polygon(6, d, rotation=30.0)
let hole = @cad.Profile2D::circle(major_d).rev()
let body = @cad.ExtrudedProfile::new(outer, height, holes=[hole])
.with_name(name_prefix)
.with_color(@cad.Rgb::orange())
// Thread ridge cross-section in the radial-axial plane: a trapezoid with the
// crest (inner radius) on the left and the root (outer radius) on the right.
let r_major = major_d / 2.0
let r_minor = minor_d / 2.0
let r_mid = (r_major + r_minor) / 2.0
let embed = 0.1
let profile = @cad.SweepProfile::new([
(-half_depth, 0.0, -crest_half),
(half_depth + embed, 0.0, -root_half),
(half_depth + embed, 0.0, root_half),
(-half_depth, 0.0, crest_half),
])
// Leave half a pitch at each end so the ridge does not stick out of the nut.
let thread_turns = (height - pitch) / pitch
if thread_turns <= 0.0 {
return [body]
}
let path = @cad.helix_path(
pos=(0.0, 0.0, pitch / 2.0),
size_per_turn=(r_mid, r_mid, pitch),
num_turns=thread_turns,
num_segs_per_turn=32,
start_angle=0.0,
clockwise=false,
)
// The thread ridge must follow the helical path.
let thread = @cad.SweptProfile::new(path~, profile~, straight_outline=false)
.with_name(name_prefix + "-thread")
.with_color(@cad.Rgb::red())
[body, thread]
}