Skip to main content
F3D
Nắp che pump Lithophane lục giác (Hex) cho Cooler Master AIO 1

7 images

Dụng Cụ Tiện Ích

Nắp che pump Lithophane lục giác (Hex) cho Cooler Master AIO

Remix of Mẫu STEP Pump AIO Elite Liquid Series By CoolerMaster

By BGDglider_2502284

Likes
1
Views
99

Files 7 194.2 MB

  • CoolerMaster_1778170163.stlSTL · 29.4 MB
  • Ice_1778170166.stlSTL · 29.4 MB
  • Kcd2_1778170168.stlSTL · 29.4 MB
  • Prusa_1778170170.stlSTL · 29.4 MB

Make a variant with AI

Build another version of this model.

Open Studio

Description

This is a hex-style pump cover for Cooler Master AIOs with a slot for custom lithophane inserts.

The cover features a clean cutout designed to hold a lithophane plate.
A Python script is included to generate lithophanes that fit precisely into this opening.

The workflow is simple: generate your image, print it, and insert it into the cover.


Lithophane Generator (Python)

You can generate the lithophane using Thonny and the provided Python script.

Required libraries:

  • PIL

  • numpy

  • numpy-stl

Script:

# Spring 2026 – Hydrolith Generator (Hex Smooth Version)

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageOps, ImageDraw, ImageTk
import numpy as np
from stl import mesh
import os

#********************** HEX PARAMETERS **********************************#

SIDE_LENGTH_MM = 28.28434
WIDTH_MM = 2 * SIDE_LENGTH_MM
HEIGHT_MM = np.sqrt(3) * SIDE_LENGTH_MM
BORDER_WIDTH_MM = 2.0

#********************** SMOOTH HEX MASK *********************************#

def hex_mask_mm(w_px, h_px, resolution, side_length_mm, scale=1.0, upscale=16):
up_w = w_px * upscale
up_h = h_px * upscale

mask_img = Image.new("L", (up_w, up_h), 0)
draw = ImageDraw.Draw(mask_img)

cx, cy = up_w / 2, up_h / 2

s_px = (side_length_mm * scale) / resolution * upscale
R = s_px

# Flat-top Hexagon
points = [
(
cx + R * np.cos(i * np.pi/3),
cy + R * np.sin(i * np.pi/3)
)
for i in range(6)
]

draw.polygon(points, fill=255)

# Anti-aliased downscale
mask = mask_img.resize((w_px, h_px), resample=Image.LANCZOS)

return np.array(mask) / 255.0 # smooth mask

#********************** Lithophane generation ***************************#

def create_lithophane(image_path, output_path, min_thick, max_thick, resolution, border_height, invert=False, progress_callback=None):
px_per_mm = 1 / resolution
size_px = (int(WIDTH_MM * px_per_mm), int(HEIGHT_MM * px_per_mm))

img_raw = Image.open(image_path).convert("L")
img = ImageOps.fit(img_raw, size_px, method=Image.LANCZOS, centering=(0.5, 0.5))
img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)

grayscale = np.asarray(img) / 255.0
if invert:
grayscale = 1.0 - grayscale

height_map = min_thick + (1.0 - grayscale) * (max_thick - min_thick)

h, w = height_map.shape

outer_mask = hex_mask_mm(w, h, resolution, SIDE_LENGTH_MM, scale=1.0)
inner_scale = 1.0 - (BORDER_WIDTH_MM / SIDE_LENGTH_MM)
inner_mask = hex_mask_mm(w, h, resolution, SIDE_LENGTH_MM, scale=inner_scale)

mask = outer_mask
rim_mask = outer_mask * (1.0 - inner_mask)

# weiche Kante → physisch glatter Übergang
height_map = min_thick + (height_map - min_thick) * mask

vertices = []
faces = []
vertex_index = {}

def get_index(x, y, ztype):
key = (x, y, ztype)
if key not in vertex_index:
if ztype == 'top':
z = height_map[y, x]
elif ztype == 'rim':
z = border_height
else:
z = 0.0
vertices.append([x * resolution, y * resolution, z])
vertex_index[key] = len(vertices) - 1
return vertex_index[key]

threshold = 0.5

for y in range(h - 1):
for x in range(w - 1):

if not (mask[y, x] > threshold and
mask[y, x+1] > threshold and
mask[y+1, x] > threshold and
mask[y+1, x+1] > threshold):
continue

def ztype(ix, iy):
return 'rim' if rim_mask[iy, ix] > threshold else 'top'

i0 = get_index(x, y, ztype(x, y))
i1 = get_index(x + 1, y, ztype(x + 1, y))
i2 = get_index(x, y + 1, ztype(x, y + 1))
i3 = get_index(x + 1, y + 1, ztype(x + 1, y + 1))

b0 = get_index(x, y, 'bottom')
b1 = get_index(x + 1, y, 'bottom')
b2 = get_index(x, y + 1, 'bottom')
b3 = get_index(x + 1, y + 1, 'bottom')

faces += [[i0, i1, i2], [i1, i3, i2]]
faces += [[b2, b1, b0], [b2, b3, b1]]
faces += [
[i0, b0, i1], [i1, b0, b1],
[i1, b1, i3], [i3, b1, b3],
[i3, b3, i2], [i2, b3, b2],
[i2, b2, i0], [i0, b2, b0]
]

if progress_callback and y % 10 == 0:
progress_callback(y / (h - 1))

vertices_np = np.array(vertices)
faces_np = np.array(faces)

litho = mesh.Mesh(np.zeros(faces_np.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces_np):
for j in range(3):
litho.vectors[i][j] = vertices_np[f[j], :]

litho.save(output_path)

#****************************** GUI **************************************#

class LithopaneApp:
def init(self, root):
self.root = root
self.root.title("Hydrolith Hex Smooth")

self.image_path = ""

tk.Button(root, text="Select Image", command=self.load_image).grid(row=0, column=0, columnspan=2, pady=10)

self.entries = {}
labels = [
("Min. Thickness (mm)", "0.8"),
("Max. Thickness (mm)", "3.2"),
("Resolution (mm/px)", "0.3"),
("Border Height (mm)", "2.0")
]

for i, (label, default) in enumerate(labels, start=1):
tk.Label(root, text=label).grid(row=i, column=0, sticky="e")
entry = tk.Entry(root)
entry.insert(0, default)
entry.grid(row=i, column=1)
self.entries[label] = entry

self.invert_var = tk.BooleanVar()
tk.Checkbutton(root, text="Invert colors (negative)", variable=self.invert_var).grid(row=5, column=0, columnspan=2)

self.status = tk.Label(root, text="", fg="blue")
self.status.grid(row=6, column=0, sticky="e")

self.progress = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
self.progress.grid(row=6, column=1, sticky="w")

tk.Button(root, text="Export STL", command=self.export_stl).grid(row=7, column=0, columnspan=2, pady=10)
tk.Button(root, text="Show Preview", command=self.show_preview).grid(row=8, column=0, columnspan=2)

def load_image(self):
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.png *.bmp")])
if file_path:
self.image_path = file_path
self.status.config(text=os.path.basename(file_path))

def update_progress(self, fraction):
self.progress["value"] = int(fraction * 100)
self.root.update_idletasks()

def export_stl(self):
if not self.image_path:
messagebox.showwarning("Warning", "Please select an image first.")
return

try:
min_thick = float(self.entries["Min. Thickness (mm)"].get())
max_thick = float(self.entries["Max. Thickness (mm)"].get())
resolution = float(self.entries["Resolution (mm/px)"].get())
border_height = float(self.entries["Border Height (mm)"].get())
except ValueError:
messagebox.showerror("Error", "Invalid input.")
return

output_path = filedialog.asksaveasfilename(defaultextension=".stl")
if not output_path:
return

create_lithophane(
self.image_path,
output_path,
min_thick,
max_thick,
resolution,
border_height,
invert=self.invert_var.get(),
progress_callback=self.update_progress
)

self.status.config(text="Done.")

def show_preview(self):
if not self.image_path:
return

resolution = float(self.entries["Resolution (mm/px)"].get())
px_per_mm = 1 / resolution
size_px = (int(WIDTH_MM * px_per_mm), int(HEIGHT_MM * px_per_mm))

img = ImageOps.fit(Image.open(self.image_path).convert("L"), size_px)
grayscale = np.asarray(img) / 255.0

mask = hex_mask_mm(size_px[0], size_px[1], resolution, SIDE_LENGTH_MM)

preview = Image.fromarray((grayscale * mask * 255).astype(np.uint8))
preview = preview.resize((300, 300))

win = tk.Toplevel(self.root)
tk_img = ImageTk.PhotoImage(preview)
tk.Label(win, image=tk_img).pack()
win.image = tk_img

# Run
if name == "main":
root = tk.Tk()
app = LithopaneApp(root)
root.mainloop()


For best Lithopane results:

  • Use 100% infill

  • Use the lowest possible layer height

This improves light diffusion and overall image clarity.

Read more Show less

Print spec

Formats
stl
Model files
7
Total size
194.2 MB
Largest file
29.4 MB

0 comments

See all