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

Nắp che pump dạng lục giác cho Cooler Master AIO, có khe gắn lithophane insert tuỳ chỉnh. Kèm script Python để tạo lithophane đúng kích thước ô khoét: tạo ảnh, in ra, rồi lắp vào nắp.

👁️
17
Lượt Xem
❤️
1
Lượt Thích
📥
0
Lượt Tải
Cập Nhật May 07, 2026
Chi tiết
Tải xuống
Bình Luận
Khoe bản in
Remix

Mô tả

Đây là nắp che pump kiểu lục giác (hex-style) cho Cooler Master AIO, có khe để gắn lithophane insert tuỳ chỉnh.

Nắp có một ô khoét gọn gàng, thiết kế để giữ một tấm lithophane.
Có kèm một script Python để tạo lithophane khớp chính xác với phần khoét này.

Quy trình rất đơn giản: tạo hình từ ảnh của bạn, in ra, rồi lắp vào nắp.


Trình tạo Lithophane (Python)

Bạn có thể tạo lithophane bằng Thonny và script Python đi kèm.

Thư viện cần có:

  • 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()

Để in Lithophane đẹp nhất:

  • Dùng 100% infill

  • Dùng layer height thấp nhất có thể

Cách này giúp ánh sáng tán đều hơn và hình nhìn rõ, nét hơn.

Giấy phép

Tác phẩm này được cấp phép theo

Creative Commons — Attribution — Share Alike

CC-BY-SA

Yêu cầu ghi công
Remix & phái sinh Được phép
Sử dụng thương mại Được phép

File mô hình

TẤT CẢ FILE MÔ HÌNH (7 Tập tin)
Đang tải files, vui lòng chờ...
Vui lòng đăng nhập để bình luận.

Chưa có bình luận nào. Hãy là người đầu tiên!

Vui lòng đăng nhập để khoe bản in của bạn.

Chưa có bản in nào được khoe. Hãy là người đầu tiên!

Remix (0)