Hex Lithophane Pump Cover – Cooler Master AIO
Obliczymy ostateczną cenę na podstawie wybranego pliku. Sprawdzisz ją i zaakceptujesz przed płatnością.
Zapisane na tym urządzeniu.
Hex pump cover with slot for custom lithophane inserts generated via included Python script.
- Ocena
- 0 / 5
- Polubienia
- 1
- Pobrania
- 3
- Wydruki
- 0
- Wyświetlenia
- 28
- Oceny
- 0
Aktualizacja u źródła:
Informacje o modelu
- Źródło
- Printables
- Kategoria
- Computers
- Licencja
- Creative Commons — Attribution — Share Alike
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.