Skip to content

Commit 946f418

Browse files
committed
feat: Stackable.
Signed-off-by: Vincent Palmer <shift@someone.section.me>
1 parent a744aa8 commit 946f418

3 files changed

Lines changed: 136 additions & 29 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# **Lenovo ThinkCentre Tiny Series Under-desk Mount**
22

3-
![Preview of the mount](./preview.png)
3+
![Preview of the mount as a singular](./preview.png)
4+
![Preview of a stack of mounts](./preview-stack.png)
45

56
A parametric OpenSCAD model designed to mount a Lenovo ThinkCentre M75q (and similar Tiny series PCs) under a desk.
67

preview-stack.png

31.5 KB
Loading

thinkcenter_mount.scad

Lines changed: 134 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
1-
// Lenovo ThinkCentre Tiny Series Under-desk Mount
1+
// Lenovo ThinkCentre Tiny Series Under-desk Mount (Stackable)
22
// Designed for 180mm x 180mm print beds (Must print diagonally!)
33
//
4-
// Print it twice. Inset M4 nut into the retaining holdes.
5-
// Mount the brackets, use a thumbscrew or grubscrew to secure.
6-
//
7-
// Print on its side, else you're going to find you're Tiny on the floor.
4+
// Usage:
5+
// 1. Print 2 brackets per PC.
6+
// 2. For the first PC (Desk Mount): Screw top tabs into desk.
7+
// 3. For the second PC (Stack): Insert nuts into the bottom tabs of the first brackets.
8+
// Use countersunk screws going UP through the top tabs of the second bracket
9+
// into the bottom tabs of the first bracket.
810

11+
/* [Preview Options] */
12+
// Enable to see the stacked assembly with PCs. Disable for printing!
13+
preview_assembly = true;
14+
// How many units to show in the preview stack
15+
preview_stack_count = 3;
16+
// Distance between the front and rear brackets in the preview
17+
bracket_spacing = 120;
918

1019
/* [Printer Settings] */
11-
// To fit 180mm bed, we rotate the part 45 degrees
12-
rotate_for_print = true;
20+
// To fit 180mm bed, we rotate the part 45 degrees (Ignored if preview_assembly is true)
21+
rotate_for_print = true;
1322

1423
/* [PC Dimensions] */
1524
// Width of the PC (usually ~179mm-182mm for Tiny series)
1625
pc_width = 183.0; // Added 1mm tolerance
1726
// Height/Thickness of the PC (usually ~37mm)
1827
pc_height = 38.0; // Added 1mm tolerance
28+
// Depth of the PC (for preview only - M-series Tiny is ~183mm)
29+
pc_real_depth = 183.0;
1930

2031
/* [Bracket Dimensions] */
2132
// Depth of a single bracket (Z-axis height during print)
22-
bracket_depth = 30;
33+
bracket_depth = 30;
2334
// Thickness of the walls
2435
wall_thick = 5;
2536
// Width of the mounting tabs
@@ -29,55 +40,77 @@ airflow_gap = 10;
2940
// Width of the retaining clips/rails that hold the PC down
3041
retainer_clip_width = 3.0;
3142

43+
/* [Stacking] */
44+
// Add mounting tabs to the bottom to allow hanging another unit
45+
enable_stacking = true;
46+
// Width of the nut for stacking (M5=~8mm, M4=~7mm)
47+
stacking_nut_width = 8.2;
48+
// Depth of the nut trap for stacking
49+
stacking_nut_depth = 3.5;
50+
3251
/* [Security] */
3352
// Diameter of the locking screw hole (M5 = 5.2, M4 = 4.2)
34-
screw_hole_dia = 4.8;
35-
// Size of the nut trap (0 to disable nut trap)
53+
screw_hole_dia = 4.8;
54+
// Size of the side lock nut trap (0 to disable nut trap)
3655
nut_trap_width = 8.2; // Standard M5 nut is ~8mm flat-to-flat
3756
nut_trap_depth = 3.5;
3857

3958
/* [Hidden] */
4059
$fn = 60;
4160

42-
module main_body() {
61+
// Added 'stacking' parameter to allow overriding the global setting per-instance
62+
module main_body(stacking = enable_stacking) {
4363
total_width = pc_width + (wall_thick * 2);
4464
// Total height now includes the bottom wall, the PC, and the airflow gap
45-
total_height = pc_height + wall_thick + airflow_gap;
46-
65+
total_height = pc_height + wall_thick + airflow_gap;
66+
4767
difference() {
4868
// Outer Shell
4969
union() {
5070
// Main U-Shape
5171
translate([-total_width/2, 0, 0])
5272
cube([total_width, total_height, bracket_depth]);
53-
54-
// Mounting Tabs (Ears)
73+
74+
// Mounting Tabs (Top Ears - To Desk)
5575
for (i = [-1, 1]) {
5676
translate([(i * (total_width/2 + tab_width/2)), total_height - wall_thick/2, bracket_depth/2])
5777
hull() {
5878
cube([tab_width, wall_thick, bracket_depth], center=true);
5979
// Fillet to main body
60-
// Shifted down (-wall_thick/2) so the top of the cylinder is flush with the top of the tab
6180
translate([-i * tab_width/2, -wall_thick/2, 0])
6281
cylinder(r=wall_thick, h=bracket_depth, center=true);
6382
}
6483
}
84+
85+
// Stacking Tabs (Bottom Ears - To Next Unit)
86+
// Uses the local parameter 'stacking' instead of global 'enable_stacking'
87+
if (stacking) {
88+
for (i = [-1, 1]) {
89+
translate([(i * (total_width/2 + tab_width/2)), wall_thick/2, bracket_depth/2])
90+
hull() {
91+
cube([tab_width, wall_thick, bracket_depth], center=true);
92+
// Fillet to main body (matching top style)
93+
translate([-i * tab_width/2, wall_thick/2, 0])
94+
cylinder(r=wall_thick, h=bracket_depth, center=true);
95+
}
96+
}
97+
}
6598
}
6699

67100
// Inner Cutout (Stepped Profile)
68101
union() {
69102
// 1. The PC Slot (Wide bottom section)
70103
translate([-pc_width/2, wall_thick, -1])
71104
cube([pc_width, pc_height, bracket_depth + 2]);
72-
105+
73106
// 2. The Airflow Gap (Narrower top section to create retaining clips)
74107
// The slot narrows by retainer_clip_width on both sides
75108
translate([-(pc_width - 2*retainer_clip_width)/2, wall_thick + pc_height - 0.1, -1])
76109
cube([pc_width - 2*retainer_clip_width, airflow_gap + 10, bracket_depth + 2]);
77110
}
78-
111+
79112
// Chamfers (Bevels) for smooth insertion
80-
113+
81114
// 1. Bottom Floor Chamfer (Existing)
82115
translate([-pc_width/2, wall_thick-10, -5])
83116
rotate([-45, 0, 0])
@@ -89,22 +122,42 @@ module main_body() {
89122
rotate([-135, 0, 0]) // Angles the cut to slope the ceiling entry
90123
cube([pc_width, 5, bracket_depth + 10]);
91124

92-
// Mounting Screw Holes (Countersunk)
125+
// Mounting Screw Holes (Top Tabs)
93126
for (i = [-1, 1]) {
94127
translate([(i * (total_width/2 + tab_width/2)), total_height + 5, bracket_depth/2])
95128
rotate([90, 0, 0]) {
96129
cylinder(d=5, h=20); // Screw shaft
97130
translate([0, 0, 5 + wall_thick])
98-
cylinder(d=10, h=10); // Screw head clearance
131+
cylinder(d=10, h=10); // Screw head clearance (Countersunk)
99132
}
100133
}
101134

135+
// Stacking Holes & Traps (Bottom Tabs)
136+
if (stacking) {
137+
for (i = [-1, 1]) {
138+
translate([(i * (total_width/2 + tab_width/2)), 0, bracket_depth/2]) {
139+
140+
// Through hole for screw from below
141+
rotate([90, 0, 0])
142+
translate([0, 0, -wall_thick - 5]) // Extend well past geometry
143+
cylinder(d=5, h=wall_thick + 10);
144+
145+
// Nut Trap (On TOP surface of bottom tab)
146+
// Located at Y=wall_thick, facing down (-Y)
147+
translate([0, wall_thick + 0.1, 0])
148+
rotate([90, 0, 0])
149+
rotate([0, 0, 30]) // Align hex flat
150+
cylinder(r=stacking_nut_width/2 / cos(30), h=stacking_nut_depth + 0.1, $fn=6);
151+
}
152+
}
153+
}
154+
102155
// Security/Locking Screw Hole (Bottom Center)
103156
translate([0, -1, bracket_depth/2])
104157
rotate([-90, 0, 0])
105158
cylinder(d=screw_hole_dia, h=wall_thick + 10);
106-
107-
// Hex Nut Trap (Optional)
159+
160+
// Hex Nut Trap (Optional Side Lock)
108161
if (nut_trap_width > 0) {
109162
translate([0, wall_thick - nut_trap_depth + 0.1, bracket_depth/2])
110163
rotate([-90, 90, 0])
@@ -113,13 +166,66 @@ module main_body() {
113166
}
114167
}
115168

116-
// Positioning logic
117-
if (rotate_for_print) {
169+
// Visual mockup of the PC for preview purposes
170+
module computer_mockup() {
171+
// Calculate PC placement relative to the bracket pair
172+
// Center the PC length (pc_real_depth) over the span of the two brackets (bracket_spacing + bracket_depth)
173+
z_start = (bracket_spacing + bracket_depth)/2 - pc_real_depth/2;
174+
175+
// A dark grey box for the PC chassis
176+
color("#333333")
177+
translate([-pc_width/2, wall_thick, z_start])
178+
cube([pc_width, pc_height, pc_real_depth]);
179+
180+
// Front Face (Lenovo Style)
181+
// Assuming positive Z is "Front"
182+
color("#111111")
183+
translate([-pc_width/2, wall_thick, z_start + pc_real_depth - 2])
184+
cube([pc_width, pc_height, 2]);
185+
186+
// Red Logo Tag
187+
color("red")
188+
translate([pc_width/2 - 25, wall_thick + 5, z_start + pc_real_depth])
189+
cube([15, 5, 0.5]);
190+
191+
// Power Button
192+
color("black")
193+
translate([pc_width/2 - 50, wall_thick + pc_height/2, z_start + pc_real_depth])
194+
rotate([0,90,0])
195+
cylinder(r=5, h=1);
196+
}
197+
198+
// Logic to display preview or print version
199+
if (preview_assembly) {
200+
// Calculate vertical offset for stacking
201+
// We want the 'top tabs' of the lower unit (Y = total_height - wall/2)
202+
// to align with 'bottom tabs' of upper unit (Y = wall/2)
203+
// Shift = wall/2 - (total_height - wall/2) = wall - total_height
204+
total_height = pc_height + wall_thick + airflow_gap;
205+
stack_offset = wall_thick - total_height;
206+
207+
for (i = [0 : preview_stack_count-1]) {
208+
translate([0, i * stack_offset, 0]) {
209+
// Determine if this layer needs stacking tabs
210+
// The last layer (bottom-most, where i == count-1) should NOT have stacking tabs
211+
// Otherwise, respect the global enable_stacking flag
212+
render_tabs = (i == preview_stack_count - 1) ? false : enable_stacking;
213+
214+
// Bracket 1 (Rear/Base)
215+
main_body(stacking=render_tabs);
216+
217+
// Bracket 2 (Front)
218+
translate([0, 0, bracket_spacing])
219+
main_body(stacking=render_tabs);
220+
221+
// The PC
222+
%computer_mockup(); // % makes it transparent/ghosted
223+
}
224+
}
225+
} else if (rotate_for_print) {
118226
// Rotates 45 degrees to fit diagonal of 180mm bed
119-
// (180^2 + 180^2)^0.5 = ~254mm diagonal space
120-
// Part width is approx 183 + 10 + 30 = ~223mm
121227
rotate([0, 0, 45])
122228
main_body();
123229
} else {
124230
main_body();
125-
}
231+
}

0 commit comments

Comments
 (0)