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


"""
Разъём питания 12VHPWR (16-pin, PCIe CEM 5.0 / 12V-2x6), две части:
  1) гнездо на плате видеокарты (корпус с защёлкой, 12 силовых + 4 сигнальных штыря);
  2) кабельный штекер (корпус с 16 полостями, терминалы, защёлка, 12 силовых + 4 сигнальных провода, оплётка).
Размеры близки к реальным (шаг 3.0 мм, корпус штекера ~20.4 мм), мелкие детали упрощены.

Запуск: Blender -> Scripting -> Создать -> вставить код -> Текст -> Запустить скрипт.
Единицы: 1 единица Blender = 1 мм. Ось вставки - Z (штекер входит сверху вниз).
"""
import bpy
import bmesh
import mathutils
from math import pi

# ---------- Настройки ----------
MATED = False        # True: штекер вставлен в гнездо; False: приподнят
GAP = 28.0           # на сколько штекер приподнят, если MATED = False, мм
WIRE_LEN = 55.0      # длина показанных проводов, мм
PITCH = 3.0          # шаг силовых контактов, мм
ROW_B = (-2.2, 0.8)  # положение двух силовых рядов по оси Y, мм
SENSE_B = 3.1        # ряд сигнальных контактов по оси Y, мм
SENSE_A = (-4.5, -1.5, 1.5, 4.5)

PREFIX = "HPWR_"


# ---------- Материалы ----------
def make_material(name, c0, c1, metallic, rough, rough_var=0.1, noise_scale=6.0,
                  stretch=(1, 1, 1), bump=0.1, detail=5.0, traces=None):
    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
        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"
            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 = {}
    m["plastic"] = make_material(PREFIX + "Plastic", (0.010, 0.010, 0.012, 1), (0.040, 0.040, 0.046, 1),
                                 0.0, 0.5, 0.15, noise_scale=40.0, bump=0.35, detail=3.0)
    m["latch"] = make_material(PREFIX + "Latch", (0.03, 0.03, 0.035, 1), (0.09, 0.09, 0.10, 1),
                               0.0, 0.4, 0.1, noise_scale=30.0, bump=0.2, detail=3.0)
    m["gold"] = make_material(PREFIX + "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["tin"] = make_material(PREFIX + "Tin", (0.55, 0.57, 0.60, 1), (0.80, 0.82, 0.85, 1),
                             1.0, 0.3, 0.1, noise_scale=15.0, bump=0.05)
    m["yellow"] = make_material(PREFIX + "WireYellow", (0.75, 0.55, 0.0, 1), (1.0, 0.78, 0.02, 1),
                                0.0, 0.35, 0.08, noise_scale=30.0, bump=0.05, detail=2.0)
    m["black"] = make_material(PREFIX + "WireBlack", (0.004, 0.004, 0.005, 1), (0.02, 0.02, 0.022, 1),
                               0.0, 0.4, 0.08, noise_scale=30.0, bump=0.05, detail=2.0)
    m["sense"] = make_material(PREFIX + "WireSense", (0.25, 0.25, 0.27, 1), (0.55, 0.56, 0.60, 1),
                               0.0, 0.4, 0.08, noise_scale=30.0, bump=0.05, detail=2.0)
    m["sleeve"] = make_material(PREFIX + "Sleeve", (0.006, 0.006, 0.007, 1), (0.05, 0.05, 0.055, 1),
                                0.0, 0.8, 0.1, noise_scale=90.0, stretch=(1.0, 1.0, 0.15), bump=0.6, detail=1.0)
    m["pcb"] = make_material(PREFIX + "PCB", (0.004, 0.004, 0.006, 1), (0.02, 0.02, 0.024, 1),
                             0.0, 0.4, 0.1, noise_scale=30.0, bump=0.15,
                             traces=((0.30, 0.18, 0.07, 1), (0.012, 0.012, 0.015, 1)))
    m["text"] = make_material(PREFIX + "Silk", (0.7, 0.7, 0.7, 1), (0.95, 0.95, 0.95, 1),
                              0.0, 0.5, 0.05, noise_scale=30.0, bump=0.02, detail=2.0)
    return m


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


# ---------- Построитель сетки ----------
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 poly(self, ab, c0, c1):
        """Призма вдоль Z по контуру (x, y)."""
        self._loop_prism([(a, b, c0) for a, b in ab], [(a, b, c1) for a, b in ab])

    def tube(self, outer, inner, c0, c1):
        n = len(outer)
        base = len(self.v)
        self.v += [(a, b, c0) for a, b in outer] + [(a, b, c1) for a, b in outer]
        self.v += [(a, b, c0) for a, b in inner] + [(a, b, c1) for a, b in inner]
        for i in range(n):
            j = (i + 1) % n
            self.f.append((base + i, base + j, base + n + j, base + n + i))
            self.f.append((base + 2 * n + i, base + 3 * n + i, base + 3 * n + j, base + 2 * n + j))
            self.f.append((base + i, base + 2 * n + i, base + 2 * n + j, base + j))
            self.f.append((base + n + i, base + n + j, base + 3 * n + j, base + 3 * n + i))

    def box(self, a0, a1, b0, b1, c0, c1):
        pts = [(a0, b0, c0), (a1, b0, c0), (a1, b1, c0), (a0, b1, c0),
               (a0, b0, c1), (a1, b0, c1), (a1, b1, c1), (a0, b1, c1)]
        base = len(self.v)
        self.v += pts
        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(base + i for i in q))

    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 rect(a0, a1, b0, b1):
    return [(a0, b0), (a1, b0), (a1, b1), (a0, b1)]


def circle(r, a0, b0, n=14):
    from math import sin, cos
    return [(a0 + r * cos(2 * pi * k / n), b0 + r * sin(2 * pi * k / n)) for k in range(n)]


def boolean_cut(obj, cutter_mb, name):
    cutter = cutter_mb.build(PREFIX + name)
    try:
        bpy.context.view_layer.update()
        mod = obj.modifiers.new("Cut", "BOOLEAN")
        mod.operation = "DIFFERENCE"
        mod.solver = "EXACT"
        mod.object = cutter
        dg = bpy.context.evaluated_depsgraph_get()
        new_mesh = bpy.data.meshes.new_from_object(obj.evaluated_get(dg))
        obj.modifiers.remove(mod)
        old = obj.data
        obj.data = new_mesh
        bpy.data.meshes.remove(old)
    except Exception as ex:
        print("Булева операция не удалась (", name, "):", ex)
    cmesh = cutter.data
    bpy.data.objects.remove(cutter, do_unlink=True)
    bpy.data.meshes.remove(cmesh)


def power_positions():
    """(a, b, ряд) для 12 силовых контактов."""
    pts = []
    for r, b in enumerate(ROW_B):
        for i in range(6):
            pts.append(((i - 2.5) * PITCH, b, r))
    return pts


# ---------- Гнездо на плате ----------
def build_header(mats):
    # плата
    pcb = MB()
    pcb.box(-26.0, 26.0, -18.0, 18.0, -1.6, 0.0)
    pcb.build(PREFIX + "PCB", mats["pcb"])

    # корпус гнезда: основание + стенки с окном под защёлку
    ia, ib = 10.45, 5.2      # внутренние полуразмеры
    oa, ob = 11.5, 6.2       # внешние полуразмеры
    H, floor = 9.5, 1.2
    sh = MB()
    sh.box(-oa, oa, -ob, ob, 0.0, floor)                                   # основание
    sh.box(-oa, oa, -ob, -ib, 0.0, H)                                      # стенка -Y
    sh.box(-oa, -ia, -ob, ob, 0.0, H)                                      # стенка -X
    sh.box(ia, oa, -ob, ob, 0.0, H)                                        # стенка +X
    sh.box(-ia, ia, ib, ob, 0.0, 3.5)                                      # стенка +Y: низ
    sh.box(-ia, ia, ib, ob, 7.5, H)                                        # стенка +Y: верх
    sh.box(-ia, -5.0, ib, ob, 3.5, 7.5)                                    # стенка +Y: левая стойка окна
    sh.box(5.0, ia, ib, ob, 3.5, 7.5)                                      # стенка +Y: правая стойка окна
    sh.box(-ia, -ia + 1.15, -ib, -ib + 0.9, floor, H)                      # ключ-выступ в углу
    sh.build(PREFIX + "Header_Shroud", mats["plastic"])

    # штыри
    pw, ps = MB(), MB()
    for a, b, _ in power_positions():
        pw.box(a - 0.7, a + 0.7, b - 0.7, b + 0.7, floor, 7.8)
    for a in SENSE_A:
        ps.box(a - 0.32, a + 0.32, SENSE_B - 0.32, SENSE_B + 0.32, floor, 6.0)
    pw.build(PREFIX + "Header_PowerPins", mats["gold"])
    ps.build(PREFIX + "Header_SensePins", mats["gold"])

    # паяные контакты снизу платы
    sol = MB()
    for a, b, _ in power_positions():
        sol.box(a - 0.9, a + 0.9, b - 0.9, b + 0.9, -2.4, -1.6)
    for a in SENSE_A:
        sol.box(a - 0.5, a + 0.5, SENSE_B - 0.5, SENSE_B + 0.5, -2.1, -1.6)
    sol.build(PREFIX + "Header_SolderJoints", mats["tin"])

    # маркировка на плате
    for body, loc, size in (("12VHPWR", (0.0, -10.5, 0.02), 2.6), ("12V-2x6", (0.0, 10.5, 0.02), 2.0)):
        cu = bpy.data.curves.new(PREFIX + "Silk_" + body, "FONT")
        cu.body = body
        cu.size = size
        cu.align_x = "CENTER"
        cu.align_y = "CENTER"
        cu.extrude = 0.01
        ob_ = bpy.data.objects.new(PREFIX + "Silk_" + body, cu)
        bpy.context.collection.objects.link(ob_)
        ob_.location = loc
        ob_.data.materials.append(mats["text"])


# ---------- Кабельный штекер ----------
def build_plug(mats):
    c0 = 1.2 + (0.0 if MATED else GAP)      # низ штекера (сторона контактов)
    H = 12.0
    outer = [(-8.2, -4.9), (10.2, -4.9), (10.2, 4.9), (-10.2, 4.9), (-10.2, -2.9)]   # со срезанным углом

    body = MB()
    body.poly(outer, c0, c0 + H)
    housing = body.build(PREFIX + "Plug_Housing")

    cut = MB()
    for a, b, _ in power_positions():
        cut.box(a - 1.1, a + 1.1, b - 1.1, b + 1.1, c0 - 1.0, c0 + 8.0)
    for a in SENSE_A:
        cut.box(a - 0.6, a + 0.6, SENSE_B - 0.6, SENSE_B + 0.6, c0 - 1.0, c0 + 6.0)
    boolean_cut(housing, cut, "PlugCutter")
    assign(housing, mats["plastic"])

    # терминалы внутри полостей
    term = MB()
    for a, b, _ in power_positions():
        term.tube(rect(a - 0.95, a + 0.95, b - 0.95, b + 0.95), rect(a - 0.6, a + 0.6, b - 0.6, b + 0.6),
                  c0 + 2.5, c0 + 8.0)
    for a in SENSE_A:
        term.tube(rect(a - 0.5, a + 0.5, SENSE_B - 0.5, SENSE_B + 0.5),
                  rect(a - 0.3, a + 0.3, SENSE_B - 0.3, SENSE_B + 0.3), c0 + 2.0, c0 + 6.0)
    term.build(PREFIX + "Plug_Terminals", mats["gold"])

    # защёлка: рычаг вдоль стенки +Y и крючок, входящий в окно гнезда
    lat = MB()
    lat.box(-4.5, 4.5, 4.9, 5.15, c0 + 3.5, c0 + H - 0.5)                    # рычаг
    lat.box(-4.5, 4.5, 4.9, 5.9, c0 + 3.5, c0 + 5.0)                          # крючок
    lat.box(-4.5, 4.5, 4.9, 6.4, c0 + H - 3.0, c0 + H - 0.5)                  # нажимная площадка
    lat.build(PREFIX + "Plug_Latch", mats["latch"])

    # провода: 6 жёлтых (+12 В), 6 чёрных (земля), 4 тонких сигнальных
    top = c0 + H
    wy, wb, ws = MB(), MB(), MB()
    for a, b, r in power_positions():
        (wy if r == 1 else wb).poly(circle(1.05, a, b, 14), top - 0.5, top + WIRE_LEN)
    for a in SENSE_A:
        ws.poly(circle(0.4, a, SENSE_B, 10), top - 0.5, top + WIRE_LEN)
    wy.build(PREFIX + "Wires_12V", mats["yellow"])
    wb.build(PREFIX + "Wires_GND", mats["black"])
    ws.build(PREFIX + "Wires_Sense", mats["sense"])

    # оплётка и переходник
    sl = MB()
    sl.tube(rect(-9.9, 9.9, -4.6, 4.6), rect(-9.4, 9.4, -4.1, 4.1), top, top + WIRE_LEN * 0.55)
    sl.build(PREFIX + "Sleeve", mats["sleeve"])
    boot = MB()
    boot.tube(rect(-10.0, 10.0, -4.7, 4.7), rect(-9.4, 9.4, -4.1, 4.1), top, top + 5.0)
    boot.build(PREFIX + "Plug_Boot", mats["latch"])


def build_scene():
    cam_data = bpy.data.cameras.new(PREFIX + "Camera")
    cam_data.lens = 70
    cam_data.clip_end = 2000.0
    cam = bpy.data.objects.new(PREFIX + "Camera", cam_data)
    bpy.context.collection.objects.link(cam)
    cam.location = (60.0, -85.0, 55.0)
    target = mathutils.Vector((0.0, 0.0, 14.0 if MATED else 24.0))
    cam.rotation_euler = (target - cam.location).to_track_quat("-Z", "Y").to_euler()
    bpy.context.scene.camera = cam

    for name, loc, rot, energy, size in (
            ("Key", (50.0, -60.0, 90.0), (pi / 3.2, 0.0, 0.5), 9000, 60),
            ("Fill", (-60.0, -40.0, 40.0), (pi / 2.6, 0.0, -0.9), 4000, 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 = rot
    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():
    for ob in list(bpy.data.objects):
        if ob.name.startswith(PREFIX):
            bpy.data.objects.remove(ob, do_unlink=True)
    mats = build_materials()
    build_header(mats)
    build_plug(mats)
    build_scene()
    print("Готово: разъём 12VHPWR")


main()