Загрузка данных


"""
Задняя планка видеокарты по фото: 3 x DP + HDMI слева, HDMI справа,
8 вентиляционных щелей, винты у портов, надписи, чёрное покрытие, процедурные текстуры.

Запуск: Blender -> Scripting -> Создать -> вставить код -> Текст -> Запустить скрипт.
Единицы: 1 единица Blender = 1 мм. Гнёзда смотрят в сторону -Y (вид спереди: Numpad 1).
Если всё тёмное: переключи 3D-вид в Material Preview (3-й шарик справа вверху).
"""
import bpy
import bmesh
import mathutils
from math import pi, sin, cos

# ---------- Настройки ----------
BLACK_BRACKET = True      # True: чёрная планка как на фото, False: шлифованный металл
PLATE_T = 1.0             # толщина планки, мм
WALL = 0.3                # толщина металлической рамки гнезда, мм
CLEAR = 0.15              # зазор для штекера, мм
DEPTH = 12.0              # глубина гнезда, мм
PCB_T = 1.6               # толщина платы, мм
COL0 = 6.25               # расстояние от левого края планки до левой колонки портов, мм

# Порты: (тип, x от левого края планки, z от нижнего края), мм
PORTS = [
    ("DP", 6.25, 97.5),
    ("DP", 6.25, 75.2),
    ("DP", 6.25, 52.9),
    ("HDMI", 6.25, 30.7),
    ("HDMI", 27.6, 30.7),
]

# Винты у портов: (x, z)
SCREWS = [(15.1, 97.6), (14.9, 75.5), (14.9, 53.7), (15.1, 31.2), (28.6, 45.3), (35.4, 31.2)]
# Два верхних отверстия (пустые)
TOP_HOLES = [(15.1, 111.1), (29.7, 111.1)]
# Вентиляционные щели: центр x, первая z, шаг, количество, размер (по X, по Z)
VENT = dict(x=28.1, z0=101.8, step=7.3, count=8, wx=16.0, hz=5.2, r=1.0)
# Нижняя пустая щель
LOWER_SLOT = dict(x=27.6, z=18.2, wx=16.5, hz=5.7, r=2.5)

# Контур планки (x от левого края, z)
OUTLINE = [
    (3.0, 0.0), (16.6, 0.0), (17.3, 1.6), (22.1, 1.6), (22.8, 0.0), (37.6, 0.0),
    (39.1, 1.5), (39.1, 16.8), (40.64, 18.3), (40.64, 104.0), (38.6, 106.0),
    (38.6, 114.0), (37.0, 115.6), (3.0, 115.6), (1.5, 114.1), (1.5, 105.0),
    (0.0, 103.5), (0.0, 17.5), (1.5, 16.0), (1.5, 1.5),
]

# Платы за портами: (x центра, z от, z до, длина по Y)
PCBS = [(6.25, 22.0, 110.0, 75.0), (27.6, 19.5, 42.5, 45.0)]

PREFIX = "GPU_"


# ---------- Материалы с процедурными текстурами ----------
def make_material(name, c0, c1, metallic, rough, rough_var=0.1, noise_scale=6.0,
                  stretch=(1.0, 1.0, 1.0), bump=0.1, traces=None, detail=6.0):
    mat = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    try:
        mat.use_nodes = True
    except Exception:
        pass
    nt = mat.node_tree
    nt.nodes.clear()
    out = nt.nodes.new("ShaderNodeOutputMaterial")
    bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
    nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    bsdf.inputs["Base Color"].default_value = c1
    bsdf.inputs["Metallic"].default_value = metallic
    bsdf.inputs["Roughness"].default_value = rough
    try:
        tc = nt.nodes.new("ShaderNodeTexCoord")
        mp = nt.nodes.new("ShaderNodeMapping")
        nt.links.new(tc.outputs["Object"], mp.inputs["Vector"])
        mp.inputs["Scale"].default_value = tuple(noise_scale * s for s in stretch)

        nz = nt.nodes.new("ShaderNodeTexNoise")
        nz.inputs["Scale"].default_value = 1.0
        nz.inputs["Detail"].default_value = detail
        nz.inputs["Roughness"].default_value = 0.6
        nt.links.new(mp.outputs["Vector"], nz.inputs["Vector"])

        ramp = nt.nodes.new("ShaderNodeValToRGB")
        ramp.color_ramp.elements[0].color = c0
        ramp.color_ramp.elements[1].color = c1
        nt.links.new(nz.outputs["Fac"], ramp.inputs["Fac"])
        color_src = ramp.outputs["Color"]

        if traces is not None:
            mp2 = nt.nodes.new("ShaderNodeMapping")
            nt.links.new(tc.outputs["Object"], mp2.inputs["Vector"])
            mp2.inputs["Scale"].default_value = (0.5, 0.5, 0.5)
            vn = nt.nodes.new("ShaderNodeTexVoronoi")
            vn.feature = "DISTANCE_TO_EDGE"
            vn.inputs["Scale"].default_value = 1.0
            nt.links.new(mp2.outputs["Vector"], vn.inputs["Vector"])
            tr = nt.nodes.new("ShaderNodeValToRGB")
            tr.color_ramp.interpolation = "CONSTANT"
            tr.color_ramp.elements[0].position = 0.0
            tr.color_ramp.elements[0].color = traces[0]
            e = tr.color_ramp.elements.new(0.04)
            e.color = traces[1]
            nt.links.new(vn.outputs["Distance"], tr.inputs["Fac"])
            color_src = tr.outputs["Color"]

        nt.links.new(color_src, bsdf.inputs["Base Color"])

        mr = nt.nodes.new("ShaderNodeMapRange")
        mr.inputs["From Min"].default_value = 0.3
        mr.inputs["From Max"].default_value = 0.7
        mr.inputs["To Min"].default_value = max(rough - rough_var, 0.02)
        mr.inputs["To Max"].default_value = min(rough + rough_var, 1.0)
        nt.links.new(nz.outputs["Fac"], mr.inputs["Value"])
        nt.links.new(mr.outputs["Result"], bsdf.inputs["Roughness"])

        bp = nt.nodes.new("ShaderNodeBump")
        bp.inputs["Strength"].default_value = bump
        bp.inputs["Distance"].default_value = 0.05
        nt.links.new(nz.outputs["Fac"], bp.inputs["Height"])
        nt.links.new(bp.outputs["Normal"], bsdf.inputs["Normal"])
    except Exception as ex:
        print("Текстуры материала", name, "не применены:", ex)
    return mat


def build_materials():
    m = {}
    if BLACK_BRACKET:
        # чёрная окрашенная сталь
        m["bracket"] = make_material("GPU_Bracket", (0.010, 0.010, 0.012, 1), (0.045, 0.045, 0.050, 1),
                                     0.3, 0.45, 0.12, noise_scale=25.0, bump=0.2, detail=3.0)
    else:
        m["bracket"] = make_material("GPU_Bracket", (0.55, 0.57, 0.60, 1), (0.82, 0.84, 0.86, 1),
                                     1.0, 0.35, 0.12, noise_scale=7.0, stretch=(8.0, 8.0, 0.25), bump=0.25)
    m["shell"] = make_material("GPU_Shell", (0.70, 0.72, 0.75, 1), (0.92, 0.93, 0.95, 1),
                               1.0, 0.22, 0.08, noise_scale=5.0, stretch=(6.0, 6.0, 0.4), bump=0.12)
    m["plastic"] = make_material("GPU_Plastic", (0.012, 0.012, 0.014, 1), (0.04, 0.04, 0.045, 1),
                                 0.0, 0.5, 0.15, noise_scale=40.0, bump=0.35, detail=3.0)
    m["gold"] = make_material("GPU_Gold", (0.80, 0.55, 0.15, 1), (1.0, 0.80, 0.30, 1),
                              1.0, 0.25, 0.1, noise_scale=20.0, bump=0.05)
    m["pcb"] = make_material("GPU_PCB", (0.01, 0.10, 0.03, 1), (0.02, 0.18, 0.05, 1),
                             0.0, 0.45, 0.1, noise_scale=30.0, bump=0.15,
                             traces=((0.55, 0.38, 0.10, 1), (0.015, 0.14, 0.04, 1)))
    m["screw"] = make_material("GPU_Screw", (0.45, 0.46, 0.50, 1), (0.78, 0.79, 0.82, 1),
                               1.0, 0.35, 0.1, noise_scale=20.0, bump=0.1)
    m["text"] = make_material("GPU_Text", (0.70, 0.70, 0.70, 1), (0.92, 0.92, 0.92, 1),
                              0.0, 0.5, 0.1, noise_scale=30.0, bump=0.05, detail=2.0)
    return m


def assign(obj, mat):
    obj.data.materials.clear()
    obj.data.materials.append(mat)


# ---------- Профили (u -> Z вверх, v -> X вправо) ----------
def hdmi_uv(w, h, c=1.0):
    return [(-w / 2, h / 2), (w / 2, h / 2), (w / 2, 0.0),
            (w / 2 - c, -h / 2), (-w / 2 + c, -h / 2), (-w / 2, 0.0)]


def dp_uv(w, h, c=1.5):
    return [(-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2 - c),
            (w / 2 - c, h / 2), (-w / 2, h / 2)]


def circle_uv(r, n=28):
    return [(r * sin(2 * pi * k / n), r * cos(2 * pi * k / n)) for k in range(n)]


def rrect_uv(hz, wx, r, seg=5):
    """Скруглённый прямоугольник: высота hz по Z, ширина wx по X."""
    pts = []
    corners = [(+1, +1, 0), (-1, +1, 90), (-1, -1, 180), (+1, -1, 270)]
    for sv, su, a0 in corners:
        cv, cu = sv * (wx / 2 - r), su * (hz / 2 - r)
        for k in range(seg + 1):
            a = (a0 + 90.0 * k / seg) * pi / 180.0
            pts.append((cu + r * sin(a), cv + r * cos(a)))   # (u, v)
    return pts


SPEC = {
    "HDMI": dict(w=13.9, h=4.45, c=1.0, prof=hdmi_uv, top=10, bot=9),
    "DP": dict(w=16.1, h=4.76, c=1.5, prof=dp_uv, top=10, bot=10),
}


def port_profiles(kind):
    s = SPEC[kind]
    wi, hi = s["w"] + CLEAR, s["h"] + CLEAR
    inner = s["prof"](wi, hi, s["c"])
    outer = s["prof"](wi + 2 * WALL, hi + 2 * WALL, s["c"])
    lip = s["prof"](wi + 2 * WALL + 1.0, hi + 2 * WALL + 1.0, s["c"])
    return s, wi, hi, inner, outer, lip


# ---------- Построитель сетки ----------
class MB:
    def __init__(self):
        self.v = []
        self.f = []

    def _loop_prism(self, a, b):
        n = len(a)
        base = len(self.v)
        self.v += a + b
        self.f.append(tuple(base + i for i in reversed(range(n))))
        self.f.append(tuple(base + n + i for i in range(n)))
        for i in range(n):
            j = (i + 1) % n
            self.f.append((base + i, base + j, base + n + j, base + n + i))

    def box(self, x0, x1, y0, y1, z0, z1):
        b = len(self.v)
        self.v += [(x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0),
                   (x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)]
        for q in ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4),
                  (2, 3, 7, 6), (1, 2, 6, 5), (3, 0, 4, 7)):
            self.f.append(tuple(b + i for i in q))

    def prism_y(self, uv, xc, zc, y0, y1):
        self._loop_prism([(xc + v, y0, zc + u) for (u, v) in uv],
                         [(xc + v, y1, zc + u) for (u, v) in uv])

    def tube_y(self, outer_uv, inner_uv, xc, zc, y0, y1):
        n = len(outer_uv)
        b = len(self.v)
        self.v += [(xc + v, y0, zc + u) for (u, v) in outer_uv]
        self.v += [(xc + v, y1, zc + u) for (u, v) in outer_uv]
        self.v += [(xc + v, y0, zc + u) for (u, v) in inner_uv]
        self.v += [(xc + v, y1, zc + u) for (u, v) in inner_uv]
        for i in range(n):
            j = (i + 1) % n
            self.f.append((b + i, b + j, b + n + j, b + n + i))
            self.f.append((b + 2 * n + i, b + 3 * n + i, b + 3 * n + j, b + 2 * n + j))
            self.f.append((b + i, b + 2 * n + i, b + 2 * n + j, b + j))
            self.f.append((b + n + i, b + n + j, b + 3 * n + j, b + 3 * n + i))

    def build(self, name, mat=None):
        mesh = bpy.data.meshes.new(name)
        mesh.from_pydata(self.v, [], self.f)
        mesh.update()
        obj = bpy.data.objects.new(name, mesh)
        bpy.context.collection.objects.link(obj)
        bm = bmesh.new()
        bm.from_mesh(mesh)
        bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
        bm.to_mesh(mesh)
        bm.free()
        if mat is not None:
            assign(obj, mat)
        return obj


def X(x_mm):
    """x от левого края планки -> мировая x (левая колонка портов в x=0)."""
    return x_mm - COL0


# ---------- Сборка ----------
def cleanup():
    for ob in list(bpy.data.objects):
        if ob.name.startswith(PREFIX):
            bpy.data.objects.remove(ob, do_unlink=True)


def build_bracket(mats):
    mb = MB()
    # контур в (u=z, v=x)
    mb.prism_y([(z, X(x)) for (x, z) in OUTLINE], 0.0, 0.0, 0.0, PLATE_T)
    plate = mb.build(PREFIX + "Bracket")

    cut = MB()
    for kind, x, z in PORTS:
        cut.prism_y(port_profiles(kind)[4], X(x), z, -1.0, PLATE_T + 1.0)
    for x, z in SCREWS:
        cut.prism_y(circle_uv(2.7), X(x), z, -1.0, PLATE_T + 1.0)
    for x, z in TOP_HOLES:
        cut.prism_y(circle_uv(2.6), X(x), z, -1.0, PLATE_T + 1.0)
    for i in range(VENT["count"]):
        cut.prism_y(rrect_uv(VENT["hz"], VENT["wx"], VENT["r"]), X(VENT["x"]),
                    VENT["z0"] - i * VENT["step"], -1.0, PLATE_T + 1.0)
    cut.prism_y(rrect_uv(LOWER_SLOT["hz"], LOWER_SLOT["wx"], LOWER_SLOT["r"]),
                X(LOWER_SLOT["x"]), LOWER_SLOT["z"], -1.0, PLATE_T + 1.0)
    cutter = cut.build(PREFIX + "Cutter")

    bpy.context.view_layer.update()
    mod = plate.modifiers.new("Holes", "BOOLEAN")
    mod.operation = "DIFFERENCE"
    mod.solver = "EXACT"
    mod.object = cutter
    dg = bpy.context.evaluated_depsgraph_get()
    new_mesh = bpy.data.meshes.new_from_object(plate.evaluated_get(dg))
    plate.modifiers.remove(mod)
    old = plate.data
    plate.data = new_mesh
    bpy.data.meshes.remove(old)

    cmesh = cutter.data
    bpy.data.objects.remove(cutter, do_unlink=True)
    bpy.data.meshes.remove(cmesh)

    bev = plate.modifiers.new("Bevel", "BEVEL")
    bev.width = 0.1
    bev.segments = 2
    assign(plate, mats["bracket"])
    return plate


def build_ports(mats):
    shell, lips, wall, tongue, pins = MB(), MB(), MB(), MB(), MB()
    tt, ct, tip = 1.0, 0.12, 2.5

    for kind, xmm, zc in PORTS:
        xc = X(xmm)
        s, wi, hi, inner, outer, lip = port_profiles(kind)
        shell.tube_y(outer, inner, xc, zc, 0.0, DEPTH + 1.0)
        lips.tube_y(lip, inner, xc, zc, -0.3, 0.0)

        for du in (-(wi / 2 - 3.0), (wi / 2 - 3.0)):
            shell.box(xc + hi / 2 - 0.35, xc + hi / 2, 2.0, 5.0, zc + du - 1.0, zc + du + 1.0)
            shell.box(xc - hi / 2, xc - hi / 2 + 0.35, 2.0, 5.0, zc + du - 1.0, zc + du + 1.0)

        wall.prism_y(inner, xc, zc, DEPTH, DEPTH + 1.0)
        tw = wi - 2.0
        tongue.box(xc - tt / 2, xc + tt / 2, tip, DEPTH, zc - tw / 2, zc + tw / 2)
        tongue.box(xc - tt / 2 - 0.1, xc + tt / 2 + 0.1, tip + 0.4, tip + 1.0,
                   zc - tw / 2 + 0.6, zc + tw / 2 - 0.6)

        for count, sign in ((s["top"], 1), (s["bot"], -1)):
            x = xc + sign * (tt / 2 + ct / 2)
            for i in range(count):
                u = zc + (i - (count - 1) / 2) * 1.0
                pins.box(x - ct / 2, x + ct / 2, tip + 0.5, DEPTH - 0.5, u - 0.25, u + 0.25)

    shell.build(PREFIX + "Shells", mats["shell"])
    lips.build(PREFIX + "Bezels", mats["shell"])
    wall.build(PREFIX + "BackWalls", mats["plastic"])
    tongue.build(PREFIX + "Tongues", mats["plastic"])
    pins.build(PREFIX + "Pins", mats["gold"])


def build_screws(mats):
    heads, slots = MB(), MB()
    for x, z in SCREWS:
        xc = X(x)
        heads.prism_y(circle_uv(2.0), xc, z, 0.3, 1.6)
        slots.box(xc - 1.3, xc + 1.3, 0.22, 0.3, z - 0.22, z + 0.22)
        slots.box(xc - 0.22, xc + 0.22, 0.22, 0.3, z - 1.3, z + 1.3)
    heads.build(PREFIX + "Screws", mats["screw"])
    slots.build(PREFIX + "ScrewSlots", mats["plastic"])


def build_pcb(mats):
    hmax = max((SPEC[k]["h"] + CLEAR) / 2 + WALL for k, _, _ in PORTS)
    pcb = MB()
    for xmm, z0, z1, length in PCBS:
        xc = X(xmm)
        pcb.box(xc - hmax - PCB_T, xc - hmax, PLATE_T + 0.3, PLATE_T + 0.3 + length, z0, z1)
    pcb.build(PREFIX + "PCB", mats["pcb"])


def add_text(body, x_mm, z, size, mat, name):
    cu = bpy.data.curves.new(PREFIX + name, "FONT")
    cu.body = body
    cu.size = size
    cu.align_x = "CENTER"
    cu.align_y = "CENTER"
    cu.extrude = 0.05
    ob = bpy.data.objects.new(PREFIX + name, cu)
    bpy.context.collection.objects.link(ob)
    ob.rotation_euler = (pi / 2, 0, 0)
    ob.location = (X(x_mm), -0.04, z)
    ob.data.materials.append(mat)
    return ob


def build_labels(mats):
    for kind, x, z in PORTS:
        if kind == "DP":
            add_text("DP", x, z - 11.0, 2.2, mats["text"], "Label_DP")
    add_text("HDMI", 6.25, 19.8, 2.2, mats["text"], "Label_HDMI_L")
    add_text("HDMI", 34.9, 24.2, 2.2, mats["text"], "Label_HDMI_R")
    add_text("Stainless Steel 304", 19.3, 5.5, 2.2, mats["text"], "Label_Steel")


def build_scene_extras():
    cam_data = bpy.data.cameras.new(PREFIX + "Camera")
    cam_data.lens = 45
    cam = bpy.data.objects.new(PREFIX + "Camera", cam_data)
    bpy.context.collection.objects.link(cam)
    cam.location = (45.0, -190.0, 95.0)
    target = mathutils.Vector((14.0, 0.0, 57.0))
    cam.rotation_euler = (target - cam.location).to_track_quat("-Z", "Y").to_euler()
    bpy.context.scene.camera = cam

    for name, loc, energy, size in (("Key", (15, -130, 120), 20000, 80), ("Fill", (90, -80, 30), 6000, 50)):
        ld = bpy.data.lights.new(PREFIX + name, "AREA")
        ld.energy = energy
        ld.size = size
        lo = bpy.data.objects.new(PREFIX + name, ld)
        bpy.context.collection.objects.link(lo)
        lo.location = loc
        lo.rotation_euler = (pi / 2.3, 0, 0.3 if name == "Fill" else 0.0)

    try:
        for area in bpy.context.screen.areas:
            if area.type == "VIEW_3D":
                area.spaces[0].shading.type = "MATERIAL"
    except Exception as ex:
        print("Не удалось переключить вид:", ex)


def main():
    cleanup()
    mats = build_materials()
    build_bracket(mats)
    build_ports(mats)
    build_screws(mats)
    build_pcb(mats)
    build_labels(mats)
    build_scene_extras()
    print("Готово: планка видеокарты по фото")


main()