Side-Release Buckle
A two-part side-release buckle for webbing straps on bags, belts and harnesses, sized by default for 25 mm webbing (set the webbing width for 20, 38 or any other size). The male part has two flexible prongs with ramped hooks and a center guide tongue; push it into the socket of the female housing and the hooks spring out into the side windows and lock, and squeezing the hooks through the windows releases it. The prong spring is designed in code: prong length, thickness, ramp angle and latch depth are parameters, the inward deflection needed to pass the socket mouth is computed from them, and the model checks that the hooks engage the walls by the full latch depth, that the latched parts do not touch, that the strap slots pass the webbing, and that the prong strain stays low enough for PLA or PETG. The female end has one strap bar; the male end has two bars and two slots so the strap can be threaded through and adjusted. Latched, the buckle measures 32 x 60 x 9.8 mm. Both parts print flat with no supports (set part to print for the layout); the prongs flex in the print plane, so they do not split along layers, and the socket roof is a 27 mm bridge.
# Side-Release Buckle - two-part snap buckle for webbing straps
#
# The classic strap buckle: a female housing with a rectangular socket and a
# window in each side wall, and a male part with two flexible prongs and a
# center guide tongue. Push the male in: the ramps on the prong hooks press
# the prongs inward, and when the hooks reach the windows they spring out and
# lock. Squeeze both hooks through the windows to release.
#
# Both ends take a webbing strap. The female end has one bar (sew or knot the
# strap around it); the male end has two bars and two slots, so the strap can
# be threaded up through one slot and down through the other and adjusted.
#
# Prong spring (rectangular cantilever, fixed at the male body):
# to pass the socket mouth each hook must move inward by the latch depth,
# so the deflection y = latch_d (the prong fit clearance is already in the
# hook height).
# strain = 1.5 * t * y / L^2, t = prong thickness, L = prong length
# default: 1.5 * 1.6 * 2.0 / 28^2 = 0.61 %, far below the 1.5 % that
# PLA / PETG take for repeated snaps. y / L = 0.071 is also kept <= 0.08
# so the small-deflection formula still applies.
#
# Print orientation: both parts lie flat as built (part = "print"), nothing
# needs supports: the prongs are extruded in Z, so they flex in the print
# plane and no layer is sheared; the windows and strap slots are vertical.
# The socket roof of the female is a bridge as wide as the socket (27 mm at
# 25 mm webbing); a cooled PLA / PETG printer bridges that cleanly.
#
# Axes: X = across the strap, Y = strap direction (the male enters along +Y),
# Z = up. The female mouth is at y = 0 in the assembled view.
@param choices:["assembled", "print", "male", "female"] desc:"Output: latched assembly, print layout, or one part"
part = "assembled"
@param 15..50 step:1 group:"Strap" desc:"Webbing width (mm); 20, 25 and 38 are the common sizes"
webbing = 25
@param 1.0..3.0 step:0.1 group:"Strap" desc:"Webbing thickness (mm); the slots are 1.5 mm wider"
webbing_t = 1.5
@param 0.5..2 step:0.1 group:"Strap" desc:"Slot length clearance over the webbing width (mm)"
strap_clear = 1.0
@param 18..40 step:1 group:"Spring" desc:"Prong length, male body to hook catch face (mm)"
prong_L = 28
@param 1.2..2.4 step:0.1 group:"Spring" desc:"Prong thickness, the bending direction (mm)"
prong_t = 1.6
@param 20..40 step:1 group:"Spring" desc:"Hook ramp angle from the insertion axis (deg)"
ramp_deg = 30
@param 1.2..2.4 step:0.1 group:"Spring" desc:"Latch depth: how far the hook enters the side wall (mm)"
latch_d = 2.0
@param 0.2..0.5 step:0.05 group:"Fit" desc:"Sliding clearance per side between male and socket (mm)"
fit = 0.3
# --- Fixed dimensions ---
wall = latch_d + 0.5 # female side wall; the window goes through it, the hook stays 0.5 inside
plate_t = 1.6 # female top and bottom plate
rail = 3.0 # material beside each strap slot
prong_h = 6.0 # prong / tongue height (Z)
fit_z = 0.3 # vertical clearance of the male inside the socket
hook_land = 1.0 # flat at full hook height, in front of the ramp
mouth_gap = 1.0 # gap between the male body and the female mouth when latched
win_play = 0.1 # window edge in front of the hook catch face
front_t = 3.0 # male front wall where the prongs are rooted
bar_t = 3.5 # male center and end bars
back_t = 2.5 # female wall between socket and strap slot
fbar_t = 4.0 # female strap bar
corner_r = 2.0 # vertical outer corners of both bodies
# --- Derived ---
slot_len = webbing + strap_clear
slot_gap = webbing_t + 1.5
w_out = slot_len + 2 * rail # outer width of both parts
w_in = w_out - 2 * wall # socket width = mouth width
h_in = prong_h + 2 * fit_z # socket height
height = h_in + 2 * plate_t # overall thickness
x_o = w_in / 2 - fit # prong outer face at rest
x_i = x_o - prong_t # prong inner face
hook_p = latch_d + fit # hook height above the prong face
ramp_len = hook_p / tan(ramp_deg)
deflect = hook_p - fit # inward travel to pass the mouth = latch_d
strain = 1.5 * prong_t * deflect / prong_L ** 2
tongue_w = max(8, w_in * 0.3)
y_front = -mouth_gap # male body front face
y_c = y_front + prong_L # hook catch face
y_tip = y_c + hook_land + ramp_len # prong tip
pocket_d = y_tip + 1.5 # socket depth
y_slot = pocket_d + back_t # female strap slot (front edge)
len_f = y_slot + slot_gap + fbar_t # female length
block_L = front_t + 2 * slot_gap + 2 * bar_t # male body length
z_lo = plate_t + fit_z # bottom of prongs and tongue
# --- Geometry checks that need no kernel ---
assert strain <= 0.015 # repeated-use strain for PLA / PETG
assert deflect / prong_L <= 0.08 # small-deflection cantilever
assert 2 * (x_o + hook_p - deflect) <= w_in # deflected hooks pass the mouth
assert x_i - deflect >= tongue_w / 2 + 1 # deflected prong clears the tongue
assert w_in / 2 + latch_d <= w_out / 2 - 0.3 # hook stays inside the side wall
assert slot_gap >= webbing_t + 0.5
print "overall thickness" height "socket" w_in "x" h_in
print "prong strain (%)" (strain * 100) "deflection / length" (deflect / prong_L)
# ===== Female =====
body_f = box w_out len_f height | floor | edges =Z | fillet corner_r
| translate 0 (len_f / 2) 0
pocket = box w_in (pocket_d + 1) h_in | floor | translate 0 ((pocket_d - 1) / 2) plate_t
slot_f = box slot_len slot_gap (height + 2) | floor | translate 0 (y_slot + slot_gap / 2) (-1)
female_solid = body_f - pocket - slot_f # before the windows: used to measure the latch
win_len = y_tip + 0.3 - (y_c - win_play)
win_one = box (wall + 2) win_len h_in | floor | translate 0 0 plate_t
windows = union [win_one | translate (s * (w_in + wall) / 2) (y_c - win_play + win_len / 2) 0 for s in [-1, 1]]
female = female_solid - windows
assert female.solids == 1
assert female.bbox.x ~ w_out
assert female.bbox.y ~ len_f
assert female.bbox.z ~ height
# ===== Male (built in the latched position) =====
body_m = box w_out block_L height | floor | edges =Z | fillet corner_r
| translate 0 (y_front - block_L / 2) 0
slot_m1 = box slot_len slot_gap (height + 2) | floor | translate 0 (y_front - front_t - slot_gap / 2) (-1)
slot_m2 = slot_m1 | translate 0 (-(slot_gap + bar_t)) 0
tongue = box tongue_w (y_c + hook_land - y_front + 1) prong_h | floor
| translate 0 ((y_c + hook_land + y_front - 1) / 2) z_lo
# +X prong as an XY profile (root 1 mm inside the body so it fuses), extruded in Z
prong_r = polyline [(x_i, y_front - 1), (x_o, y_front - 1), (x_o, y_c), (x_o + hook_p, y_c),
(x_o + hook_p, y_c + hook_land), (x_o, y_tip), (x_i, y_tip)]
| extrude prong_h | translate 0 0 z_lo
prong_l = prong_r | mirror "X"
male = body_m - slot_m1 - slot_m2 + tongue + prong_r + prong_l
assert male.solids == 1
assert male.bbox.x ~ w_out
assert male.bbox.y ~ (y_tip - (y_front - block_L))
assert male.bbox.z ~ height
# ===== Latched state =====
assert clear(male, female) # hooks sit in the windows, nothing collides
latch = male * female_solid # the hooks inside the side walls
assert latch.solids == 2
latch_r = latch * (box 20 100 100 | translate (w_in / 2 + 10) 0 0)
# Along Y the hook is inside the wall over the land plus the part of the ramp
# that stands higher than the fit clearance
in_wall_len = hook_land + ramp_len * latch_d / hook_p
print "latch engagement (mm)" latch_r.bbox.x "hook in wall (mm)" latch_r.bbox.y
assert latch_r.bbox.x ~ latch_d tol:0.01
assert latch_r.bbox.y ~ in_wall_len tol:0.05
# Strap probes: a webbing cross-section passes straight through each slot
probe = box (webbing + strap_clear - 0.01) webbing_t (height + 4) | floor | translate 0 0 (-2)
assert clear(probe | translate 0 (y_slot + slot_gap / 2) 0, female)
assert clear(probe | translate 0 (y_front - front_t - slot_gap / 2) 0, male)
assert clear(probe | translate 0 (y_front - front_t - slot_gap * 1.5 - bar_t) 0, male)
asm = male + female
print "assembled size" asm.bbox.x asm.bbox.y asm.bbox.z
print "female size" female.bbox.x female.bbox.y female.bbox.z
print "male size" male.bbox.x male.bbox.y male.bbox.z
# ===== Print layout: both flat, side by side =====
male_print = male | translate (w_out + 8) (-(y_front - block_L)) 0
female_print = female
layout = female_print + male_print
assert layout.solids == 2
if part == "assembled" then asm
else if part == "print" then layout
else if part == "male" then (male | translate 0 (-(y_front - block_L)) 0)
else female