Under-Desk Cable Tray
A cable management tray that screws to the underside of a desk to hold a power strip, adapters and cable slack. The J-shaped channel hangs from a tall back wall whose screw tabs take 3.5 or 4 mm countersunk wood screws, while the lower front wall leaves a 25 mm gap below the desk so cables and plugs can be dropped in after mounting. A thickened front lip, 45-degree inner corner chamfers and two end walls with U-shaped cable notches keep the open channel stiff under about 2 kg. The default size is 200 x 81 x 68 mm, which fits a 220 mm print bed. Print it upright with the open side up: the tab gussets, lip and countersinks are all 45 degrees, so no supports are needed. Adjustable parameters: length, inner width, depth, drop-in gap, wall thickness, screw size, number and spacing of screws, notch width and height, and optional floor slots.
# Under-Desk Cable Tray - J-profile channel with countersunk screw tabs, prints upright with no supports
#
# Mounting: the top of the tall back wall sits against the underside of the desk and
# the tabs behind it are screwed up into the desktop with countersunk wood screws
# (3.5 or 4 mm, about 16-20 mm long for an 18-25 mm desk top). The front wall is lower,
# so a gap of `gap` mm stays open between the front lip and the desk: cables and plugs
# drop in from above after the tray is mounted. The end walls have U notches for the
# cables to leave the tray.
#
# Coordinates (= mounted orientation = print orientation): X = length (centered),
# Y = across the tray (y = 0 is the back face that goes toward the wall/desk rear),
# Z = up toward the desk. The floor lies on Z = 0.
#
# Print upright, open side up. Every overhang is 45 deg or steeper: the screw tabs have a
# 45 deg gusset underneath, the front lip and the inner corner chamfers are 45 deg, and the
# countersinks open downward at 90 deg (45 deg per side).
#
# Strength assumption: up to 2 kg of load (a 6-outlet power strip of about 0.6 kg plus
# adapters and cable slack, with a x2 margin), PLA or PETG. The back wall is a 68 mm deep
# beam between the screws; the floor is braced by 45 deg chamfers in both inner corners,
# a thickened lip on the free front edge and the two end walls, which keep the
# open channel from twisting or the front edge from sagging.
@param 120..220 step:5 desc:"Overall length (mm). 220 mm or less fits common print beds" group:"Size"
length = 200
@param 30..120 desc:"Inner width of the channel (mm)" group:"Size"
inner_w = 60
@param 20..80 desc:"Inner depth, floor to front lip (mm)" group:"Size"
depth = 40
@param 10..50 desc:"Drop-in gap between the front lip and the desk (mm)" group:"Size"
gap = 25
@param 2..5 step:0.5 desc:"Wall and floor thickness (mm)" group:"Size"
wall = 3
@param choices:["3.5", "4"] desc:"Countersunk wood screw diameter (mm)" group:"Mounting"
screw = "4"
@param 2..4 desc:"Number of screw tabs" group:"Mounting"
screw_count = 2
@param 40..200 step:5 desc:"Distance between neighbouring screws (mm)" group:"Mounting"
screw_spacing = 150
@param 10..60 desc:"Width of the cable notch in each end wall (mm)" group:"Cable exits"
notch_w = 30
@param 0..30 desc:"Height of the notch bottom above the floor (mm)" group:"Cable exits"
notch_floor = 10
@param desc:"Lightening slots in the floor" group:"Floor"
floor_slots = true
# --- Derived sizes ---
t = wall
ht = wall + depth + gap # back wall height = overall height
lip_z = wall + depth # top of the front wall
yo = 2 * wall + inner_w # outer face of the front wall
c = 6 # 45 deg chamfer in the inner floor corners
lb = wall # inward lip on the front edge (doubles the wall there)
# --- Wood screw (DIN 7997 style countersunk head, 90 deg) ---
thru_d = if screw == "3.5" then 4.0 else 4.5 # shank clearance
head_d = (if screw == "3.5" then 7.0 else 8.0) + 0.4 # head diameter + relief
csk_h = (head_d - thru_d) / 2 # 90 deg: depth = radius difference
seat = 3 # plastic clamped under the head
# --- Screw tab: 15 mm behind the back wall, 5 mm thick at the tip, 45 deg gusset below ---
tab_r = 15 # reach behind the back wall
tab_t = 5 # thickness at the tip
tab_w = 18 # width along X
sy = -8 # screw axis, 8 mm behind the back face
# --- Channel: one J-shaped profile in the YZ plane, extruded along X ---
profile = workplane YZ | polyline [
(0, 0), (yo, 0), (yo, lip_z),
(t + inner_w - lb, lip_z), (t + inner_w - lb, lip_z - 2),
(t + inner_w, lip_z - 2 - lb),
(t + inner_w, t + c), (t + inner_w - c, t),
(t + c, t), (t, t + c),
(t, ht), (0, ht)
]
channel = profile | extrude length | translate (length / -2) 0 0
# --- End walls with a U notch (round bottom, open at the top) ---
cap_h = lip_z - t
cap = box t (inner_w + 1) (cap_h + 0.5) | floor | translate 0 (t + inner_w / 2) (t - 0.5)
nz = t + notch_floor + notch_w / 2 # center of the notch's round bottom
notch = union [
workplane YZ | circle (notch_w / 2) | extrude (t * 4) | translate (t * -2) (t + inner_w / 2) nz,
box (t * 4) notch_w (ht) | floor | translate 0 (t + inner_w / 2) nz
]
xe = length / 2 - t / 2
end = cap - notch
ends = union [end | translate xe 0 0, end | translate (xe * -1) 0 0]
# --- Screw tabs ---
tab = workplane YZ | polyline [
(0.5, ht), (-tab_r, ht), (-tab_r, ht - tab_t),
(0.5, ht - tab_t - tab_r - 0.5)
] | extrude tab_w | translate (tab_w / -2) 0 0
| edges =Z <Y | fillet 2
sx0 = (screw_count - 1) * screw_spacing / -2
sxs = [sx0 + i * screw_spacing for i in range(screw_count)]
tabs = union [tab | translate x 0 0 for x in sxs]
# Countersunk hole, head side down: shank through the top, cone, then a bore of head
# diameter down through the gusset so the screwdriver and the head reach the seat.
cone_top = ht - seat
cone_bot = cone_top - csk_h
csk = union [
cylinder (thru_d / 2) (seat + 2) | floor | translate 0 0 (cone_top - 0.5),
cone (head_d / 2) (thru_d / 2) csk_h | floor | translate 0 0 cone_bot,
cylinder (head_d / 2) (tab_r + 10) | floor | translate 0 0 (cone_bot - tab_r - 10 + 0.01)
]
holes = union [csk | translate x sy 0 for x in sxs]
# --- Floor slots (stadium shaped, across the tray) ---
slot_w = 6
slot_l = inner_w - 2 * c - 6
slot_pitch = 16
n_slots = floor((length - 2 * t - 24) / slot_pitch) + 1
slot = union [
box slot_w (slot_l - slot_w) (t * 3),
cylinder (slot_w / 2) (t * 3) at:(0, (slot_l - slot_w) / 2),
cylinder (slot_w / 2) (t * 3) at:(0, (slot_l - slot_w) / -2)
] | translate 0 (t + inner_w / 2) 0
slx0 = (n_slots - 1) * slot_pitch / -2
slots = union [slot | translate (slx0 + i * slot_pitch) 0 0 for i in range(n_slots)]
body = channel + ends + tabs
tray = if floor_slots then (body - holes - slots) else (body - holes)
# --- Intent ---
assert tray.solids == 1
assert tray.valid
assert tray.bbox.x ~ length
assert tray.bbox.y ~ (yo + tab_r)
assert tray.bbox.z ~ ht
assert tray.bbox.min[2] ~ 0 tol:0.01
assert (screw_count - 1) * screw_spacing + tab_w <= length - 2 * t
assert notch_floor + notch_w / 2 < depth # notch bottom is below the lip
assert sy - head_d / 2 > -tab_r + 1.5 # head bore stays inside the tab
assert sy + head_d / 2 < -1 # and clear of the back wall
assert slot_l > slot_w
print "screws" screw_count "at x =" sxs
print "slots" (if floor_slots then n_slots else 0)
print "size" tray.bbox
tray