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


# -*- coding: utf-8 -*-
"""
КОНВЕЙЕР v2: лента обтягивает ТВОЮ плиту, зубцы большие плоские шевроны, валов нет.

Как запустить:
  1. Выдели свою ПЛИТУ (ту, вокруг которой должна обвиваться лента) - кликни по ней.
     Если не выделять - лента построится по размерам из настроек ниже.
  2. Scripting -> Text Editor -> New -> вставить код -> Alt+P
  3. Свои старые зубцы (Cube.002) удали или спрячь - теперь зубцы делает скрипт.

Что делает:
  * Берёт размеры плиты (длина, ширина, толщина) и её положение/поворот
  * Лента = тонкая полоса + шевроны, замкнутая петля вплотную вокруг плиты
    (радиус изгиба = половина толщины плиты, поэтому концы тонкие, как у прокатного ролика)
  * Зубцы и полоса гнутся модификатором Curve вместе -> зубцы прижаты и обвивают плиту
  * Анимация: лента бесконечно едет по кругу
"""
import math
import bpy
import bmesh
from mathutils import Vector, Euler

# ============================  НАСТРОЙКИ  ============================
BED_NAME = ""          # имя плиты в Outliner (например "Cube"). Пусто = берётся выделенный объект

# Эти размеры работают, ТОЛЬКО если плита не выделена и BED_NAME пустой:
BED_LENGTH = 4.5       # длина плиты
BED_WIDTH  = 1.5       # ширина плиты
BED_THICK  = 0.10      # толщина плиты (от неё зависит радиус закругления на концах)

BELT_THICK = 0.012     # толщина самой ленты (полосы под зубцами)
CLEARANCE  = 0.001     # зазор между лентой и плитой
MIN_RADIUS = 0.01      # минимальный радиус изгиба (если плита очень тонкая)

# Зубцы. Размеры в долях от ширины ленты, высота - в метрах
RIB_HEIGHT     = 0.007   # высота зубца (у тебя на скрине 0.00687)
RIB_THICK_REL  = 0.16    # толщина зубца вдоль ленты = 16% ширины ленты
RIB_PITCH_REL  = 0.24    # шаг между зубцами = 24% ширины (зазор = шаг - толщина)
RIB_MARGIN_REL = 0.015   # отступ зубцов от краёв ленты
RIB_ANGLE_DEG  = 45.0    # 45 = угол шеврона 90 градусов. Меньше = тупее, больше = острее
RIB_ROUND      = 0.0025  # скругление краёв зубца (0 = острые грани)
DIRECTION      = 1       # 1 = острие шеврона по ходу ленты, -1 = против

MAX_TILE_VERTS = 30000   # ограничитель плотности сетки (чтобы Блендер не завис)
PERIOD_FRAMES  = 48      # кадров на один шаг зубца (меньше = быстрее едет)
LOOPS          = 5       # сколько шагов проедет за анимацию (длина = PERIOD_FRAMES*LOOPS кадров)

# --- ВЫГРУЗКА В UNITY ---
BAKE_FOR_EXPORT = True   # True = запечь модификаторы в меш (нужно для нормального FBX-экспорта в Unity)
# =====================================================================

K = 0.5522847498
GENERATED = ("Conveyor_Root", "Belt_Path", "Belt_Plate", "Belt_Ribs")


def make_material(name, color, roughness):
    mat = bpy.data.materials.new(name)
    try:
        mat.use_nodes = True
    except Exception:
        pass
    try:
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf is not None:
            bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
            bsdf.inputs["Roughness"].default_value = roughness
    except Exception as err:
        print("материал: не вышло настроить ноды:", err)
    mat.diffuse_color = (color[0], color[1], color[2], 1.0)
    return mat


def finish_mesh(bm, me, material):
    for f in bm.faces:
        f.smooth = True
    bm.to_mesh(me)
    bm.free()
    if hasattr(me, "use_auto_smooth"):
        me.use_auto_smooth = True
    me.materials.append(material)
    me.update()


# ---------------------------  ПЛИТА-ЦЕЛЬ  ---------------------------
def pick_bed():
    if BED_NAME:
        ob = bpy.data.objects.get(BED_NAME)
        if ob is None or ob.type != 'MESH':
            print("Объект %r не найден или не меш - строю по размерам из настроек" % BED_NAME)
            return None
        return ob
    ob = bpy.context.active_object
    if ob is not None and ob.type == 'MESH' and ob.name not in GENERATED and ob.select_get():
        return ob
    return None


def fit_from_bed(ob):
    """Центр, поворот, длина, ширина и толщина плиты."""
    d = ob.dimensions
    lx, ly, lz = d.x, d.y, d.z
    mw = ob.matrix_world
    corners = [Vector(c) for c in ob.bound_box]
    center_local = sum(corners, Vector((0.0, 0.0, 0.0))) / 8.0
    center = mw @ center_local
    _, quat, _ = mw.decompose()
    if lx >= ly:
        length, width = lx, ly
    else:
        length, width = ly, lx      # плита лежит длинной стороной вдоль Y - поворачиваем ленту
        quat = quat @ Euler((0.0, 0.0, math.radians(90.0)), 'XYZ').to_quaternion()
    return center, quat, length, width, lz


# ---------------------------  КРИВАЯ-ПУТЬ  ---------------------------
def build_path(L, R):
    cu = bpy.data.curves.new("Belt_Path", 'CURVE')
    cu.dimensions = '2D'
    cu.resolution_u = 64
    sp = cu.splines.new('BEZIER')
    sp.bezier_points.add(5)
    sp.use_cyclic_u = True

    s = L / 3.0
    kr = K * R
    pts = [
        ((-L / 2, R),      (-L / 2 - kr, R),  (-L / 2 + s, R)),
        ((L / 2, R),       (L / 2 - s, R),    (L / 2 + kr, R)),
        ((L / 2 + R, 0),   (L / 2 + R, kr),   (L / 2 + R, -kr)),
        ((L / 2, -R),      (L / 2 + kr, -R),  (L / 2 - s, -R)),
        ((-L / 2, -R),     (-L / 2 + s, -R),  (-L / 2 - kr, -R)),
        ((-L / 2 - R, 0),  (-L / 2 - R, -kr), (-L / 2 - R, kr)),
    ]
    for bp, (co, hl, hr) in zip(sp.bezier_points, pts):
        bp.handle_left_type = 'FREE'
        bp.handle_right_type = 'FREE'
        bp.co = (co[0], co[1], 0.0)
        bp.handle_left = (hl[0], hl[1], 0.0)
        bp.handle_right = (hr[0], hr[1], 0.0)

    obj = bpy.data.objects.new("Belt_Path", cu)
    obj.rotation_euler = (math.radians(90.0), 0.0, 0.0)   # НЕ применять (Ctrl+A не жать)
    return obj


def measure_curve(obj, fallback):
    try:
        bpy.context.view_layer.update()
        dg = bpy.context.evaluated_depsgraph_get()
        ev = obj.evaluated_get(dg)
        me = ev.to_mesh()
        total = 0.0
        if me is not None:
            for e in me.edges:
                a = me.vertices[e.vertices[0]].co
                b = me.vertices[e.vertices[1]].co
                total += (a - b).length
        ev.to_mesh_clear()
        if total > 0.8 * fallback:
            return total
    except Exception as err:
        print("measure_curve: не вышло, беру формулу:", err)
    return fallback


# ---------------------------  ПОЛОСА ЛЕНТЫ  ---------------------------
def build_plate(length, W, TH, material):
    me = bpy.data.meshes.new("Belt_Plate")
    bm = bmesh.new()
    n = max(2, int(math.ceil(length / PLATE_SEG)))
    hw = W / 2.0
    profile = [(-hw, 0.0), (hw, 0.0), (hw, TH), (-hw, TH)]
    rings = []
    for i in range(n + 1):
        x = length * i / n
        rings.append([bm.verts.new((x, y, z)) for (y, z) in profile])
    for i in range(n):
        a, b = rings[i], rings[i + 1]
        for k in range(4):
            bm.faces.new((a[k], a[(k + 1) % 4], b[(k + 1) % 4], b[k]))
    finish_mesh(bm, me, material)
    return me


# ---------------------------  ЗУБЕЦ-ШЕВРОН  ---------------------------
def build_rib_tile(W, TH, pitch, seg, material):
    """Один большой плоский шеврон. Сетка мелкая и вдоль, и поперёк зубца,
    чтобы широкий зубец нормально огибал тонкий изгиб, а не срезал его."""
    me = bpy.data.meshes.new("Belt_Ribs")
    bm = bmesh.new()

    hw = W / 2.0 - RIB_MARGIN_REL * W
    a = hw * math.tan(math.radians(RIB_ANGLE_DEG))
    t = RIB_THICK_REL * W
    span = a + t
    if t + 0.001 > pitch:
        print("ВНИМАНИЕ: зубцы толще шага - будут слипаться. Увеличь RIB_PITCH_REL или уменьши RIB_THICK_REL.")
    x_off = 0.0 if span >= pitch else (pitch - span) / 2.0

    embed = 0.3 * TH
    z0 = TH - embed
    z1 = TH + RIB_HEIGHT

    while True:
        ny = max(4, int(math.ceil(a / seg)))
        nx = max(2, int(math.ceil(t / seg)))
        if (2 * ny + 1) * (2 * nx + 2) <= MAX_TILE_VERTS:
            break
        seg *= 1.15

    rows = []
    for j in range(-ny, ny + 1):
        y = hw * j / ny
        d = a * abs(j) / ny
        xl = span - d                 # передняя кромка
        xt = xl - t                   # задняя кромка
        if DIRECTION < 0:
            xl, xt = span - xl, span - xt
        xl += x_off
        xt += x_off
        ring = []
        for i in range(nx + 1):       # низ: от передней кромки к задней
            ring.append(bm.verts.new((xl + (xt - xl) * i / nx, y, z0)))
        for i in range(nx, -1, -1):   # верх: обратно
            ring.append(bm.verts.new((xl + (xt - xl) * i / nx, y, z1)))
        rows.append(ring)

    M = len(rows[0])
    for j in range(len(rows) - 1):
        r0, r1 = rows[j], rows[j + 1]
        for k in range(M):
            bm.faces.new((r0[k], r0[(k + 1) % M], r1[(k + 1) % M], r1[k]))
    bm.faces.new(rows[0])
    bm.faces.new(rows[-1][::-1])
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    finish_mesh(bm, me, material)
    return me, seg


# ---------------------------  МОДИФИКАТОРЫ  ---------------------------
def add_mods(obj, path_obj, bevel_width, bevel_segments, pitch=None, count=None):
    if bevel_width > 0:
        bev = obj.modifiers.new("Bevel", 'BEVEL')
        bev.width = bevel_width
        bev.segments = bevel_segments
        bev.limit_method = 'ANGLE'
        bev.angle_limit = math.radians(35.0)
        try:
            bev.profile = 0.5
        except Exception:
            pass

    if pitch is not None:                      # Array строго ДО Curve
        arr = obj.modifiers.new("Array", 'ARRAY')
        arr.fit_type = 'FIXED_COUNT'
        arr.count = count
        arr.use_relative_offset = False
        arr.use_constant_offset = True
        arr.constant_offset_displace = (pitch, 0.0, 0.0)

    cur = obj.modifiers.new("Curve", 'CURVE')
    cur.object = path_obj
    cur.deform_axis = 'POS_X'

    try:
        wn = obj.modifiers.new("WeightedNormal", 'WEIGHTED_NORMAL')
        wn.keep_sharp = True
        wn.weight = 50
    except Exception as err:
        print("WeightedNormal пропущен:", err)


# ---------------------------  ЗАПЕКАНИЕ ДЛЯ ЭКСПОРТА  ---------------------------
def bake_for_export(objs):
    """Превращает модификаторы (Array, Curve, Bevel, WeightedNormal) в реальную геометрию.
    После этого FBX-экспорт в Unity проходит мгновенно, без зависаний.
    Анимация (Location X) остаётся - лента так же едет по кругу."""
    bpy.ops.object.select_all(action='DESELECT')
    for o in objs:
        o.select_set(True)
    bpy.context.view_layer.objects.active = objs[0]

    for o in objs:
        # применяем все модификаторы через convert (работает и с Curve/Array)
        bpy.context.view_layer.objects.active = o
        bpy.ops.object.convert(target='MESH', keep_original=False)

    # Belt_Path больше не нужен для деформации - скрываем (но оставляем в сцене, чтобы ничего не сломать)
    path_obj = bpy.data.objects.get("Belt_Path")
    if path_obj is not None:
        path_obj.hide_viewport = True
        path_obj.hide_render = True
        path_obj.hide_set(True)

    print("Модификаторы запечены в меш - FBX-экспорт в Unity теперь быстрый.")


# =====================  СБОРКА  =====================
scene = bpy.context.scene

bed = pick_bed()          # берём плиту ДО чистки прошлого запуска

if bed is not None:
    center, quat, bed_len, W, bed_thk = fit_from_bed(bed)
    print("Подгоняю под объект %r: длина %.3f, ширина %.3f, толщина %.3f" % (bed.name, bed_len, W, bed_thk))
    if bed_thk <= 0.0 or bed_thk > 0.35 * min(bed_len, W):
        print("ВНИМАНИЕ: толщина плиты выглядит странно (в этом объекте ножки/каркас?). "
              "Беру BED_THICK = %.3f. Если это не то, задай BED_THICK руками." % BED_THICK)
        bed_thk = BED_THICK
else:
    center, quat = None, None
    bed_len, W, bed_thk = BED_LENGTH, BED_WIDTH, BED_THICK
    print("Плита не выделена - строю по размерам из настроек.")

R = max(bed_thk / 2.0 + CLEARANCE, MIN_RADIUS)     # радиус изгиба = половина толщины плиты
TH = max(BELT_THICK, 0.45 * R)                     # чтобы углы плиты прятались внутри ленты
L = bed_len - 2.0 * R                              # прямой участок между центрами изгибов
if L < 0.1:
    print("ВНИМАНИЕ: плита слишком короткая для такой толщины.")
    L = 0.1

# сетка: чем меньше радиус, тем мельче нужна нарезка
PLATE_SEG = min(0.03, max(0.003, R * 0.2))

for nm in GENERATED:
    old = bpy.data.objects.get(nm)
    if old is not None:
        bpy.data.objects.remove(old, do_unlink=True)

coll = bpy.data.collections.get("Conveyor")
if coll is None:
    coll = bpy.data.collections.new("Conveyor")
if coll.name not in scene.collection.children:
    scene.collection.children.link(coll)

root = bpy.data.objects.new("Conveyor_Root", None)
root.empty_display_type = 'PLAIN_AXES'
coll.objects.link(root)
if center is not None:
    root.rotation_mode = 'QUATERNION'
    root.rotation_quaternion = quat
    root.location = center
else:
    root.location = (0.0, 0.0, R + TH)             # чтобы без плиты лента стояла на сетке

path_obj = build_path(L, R)
coll.objects.link(path_obj)
path_obj.parent = root

ideal_len = 2 * L + 2 * math.pi * R
path_len = measure_curve(path_obj, ideal_len)

want_pitch = RIB_PITCH_REL * W
count = max(1, int(round(path_len / want_pitch)))
pitch = path_len / count                           # целое число зубцов на весь периметр

mat_plate = make_material("Belt_Rubber", (0.045, 0.045, 0.05), 0.65)
mat_rib = make_material("Belt_Ribs_Mat", (0.16, 0.16, 0.18), 0.55)

plate_obj = bpy.data.objects.new("Belt_Plate", build_plate(path_len + 0.01, W, TH, mat_plate))
rib_mesh, seg_used = build_rib_tile(W, TH, pitch, PLATE_SEG, mat_rib)
ribs_obj = bpy.data.objects.new("Belt_Ribs", rib_mesh)
if seg_used > PLATE_SEG * 1.01:
    print("Сетку зубцов пришлось огрубить до %.4f (иначе слишком тяжело). "
          "Если на изгибе видны грани - увеличь MAX_TILE_VERTS." % seg_used)

for o in (plate_obj, ribs_obj):
    coll.objects.link(o)
    o.parent = root

add_mods(plate_obj, path_obj, min(0.003, TH * 0.25), 2)
rib_round = min(RIB_ROUND, RIB_HEIGHT * 0.45)
add_mods(ribs_obj, path_obj, rib_round, 3, pitch=pitch, count=count)

# =====================  АНИМАЦИЯ  =====================
prefs = bpy.context.preferences.edit
old_interp = prefs.keyframe_new_interpolation_type
prefs.keyframe_new_interpolation_type = 'LINEAR'

last = 1 + PERIOD_FRAMES * LOOPS
for o in (plate_obj, ribs_obj):
    o.location.x = 0.0
    o.keyframe_insert(data_path="location", index=0, frame=1)
    o.location.x = pitch * LOOPS
    o.keyframe_insert(data_path="location", index=0, frame=last)
    o.location.x = 0.0

prefs.keyframe_new_interpolation_type = old_interp

scene.frame_start = 1
scene.frame_end = PERIOD_FRAMES * LOOPS
scene.frame_set(1)

# =====================  ЗАПЕКАНИЕ ПОД UNITY  =====================
if BAKE_FOR_EXPORT:
    # Возвращаемся на 1-й кадр, чтобы запекание шло из исходного положения
    scene.frame_set(1)
    bake_for_export([plate_obj, ribs_obj])

print("Готово! Радиус изгиба %.3f, периметр %.3f, зубцов %d, шаг %.3f" % (R, path_len, count, pitch))