# 로터리 다이얼 전화기 (1950년대 Western Electric 500형 탁상 전화기를 본뜬 모델)
# - 본체: 옆 단면(앞쪽이 비스듬히 깎인 프로필)을 X 방향으로 돌출하고, 위로 갈수록 좁아지게
#   선형으로 기울인 뒤 큰 베벨로 모서리를 둥글린 셸이다. 아래에 금속 바닥판을 둔다.
# - 다이얼: 경사면 위에 기울인 빈 오브젝트를 기준으로 번호판·숫자(3D 글자)·손가락 구멍 10개를
#   불리언으로 뚫은 회전판·크롬 베젤·손가락 멈춤쇠를 원형 배치했다.
# - 수화기: 손잡이는 타원 단면을 곡선 경로로 쓸어 만든 관(sweep), 송화구·수화구 컵은 회전체(lathe).
# - 꼬인 코드: 부드러운 스플라인 경로를 따라 평행 이동 프레임으로 나선을 감아 만든 관이다.
# 실제 크기(폭 약 21cm, 깊이 약 22cm, 높이 약 16cm), 단위 미터.
from atelier import *

SHOT = {"azimuth": -58, "elevation": 22, "lens": 60, "light": "studio", "background": "#2a323d"}

# ---------------------------------------------------------------- 재질
RED = mat('Bakelite_Red', '#9b1b20', rough=0.22, coat=0.6)
CREAM = mat('Number_Plate_Cream', '#ebe3cf', rough=0.45)
INK = mat('Printed_Ink', '#26262a', rough=0.6)
CHROME = mat('Chrome', '#cfd2d6', rough=0.12, metal=1.0)
BASE = mat('Base_Plate_Steel', '#3b3d42', rough=0.5, metal=0.6)
RUBBER = mat('Grommet_Rubber', '#2e2a2a', rough=0.7)
PLUNGER = mat('Plunger_Ivory', '#d8d0bd', rough=0.35)
GRILLE = mat('Receiver_Grille', '#5a1014', rough=0.5)

# ---------------------------------------------------------------- 공용 함수


def to_mesh(ob):
    """커브/글자 오브젝트를 같은 이름의 메시로 바꾼다(GLB 호환)."""
    dg = bpy.context.evaluated_depsgraph_get()
    me = bpy.data.meshes.new_from_object(ob.evaluated_get(dg))
    nob = bpy.data.objects.new(ob.name, me)
    link(nob)
    nob.matrix_world = ob.matrix_world.copy()
    nob.parent = ob.parent
    if ob.parent:
        nob.matrix_parent_inverse = ob.matrix_parent_inverse.copy()
    name = ob.name
    bpy.data.objects.remove(ob, do_unlink=True)
    nob.name = name
    return nob


def sharp_smooth(ob, angle=35):
    """부드러운 음영 + 각진 모서리만 날카롭게."""
    for p in ob.data.polygons:
        p.use_smooth = True
    ob.data.set_sharp_from_angle(angle=math.radians(angle))
    return ob


def catmull(points, samples=12):
    """Catmull-Rom 스플라인으로 점열을 부드럽게."""
    P = [Vector(p) for p in points]
    P = [P[0] * 2 - P[1]] + P + [P[-1] * 2 - P[-2]]
    out = []
    for i in range(1, len(P) - 2):
        p0, p1, p2, p3 = P[i - 1], P[i], P[i + 1], P[i + 2]
        for s in range(samples):
            t = s / samples
            out.append(0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t
                              + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t))
    out.append(P[-2])
    return out


def resample(pts, step):
    """호 길이 기준으로 일정 간격 재표본화."""
    out = [pts[0]]
    acc = 0.0
    for a, b in zip(pts, pts[1:]):
        seg = (b - a).length
        while acc + seg >= step:
            t = (step - acc) / seg
            a = a.lerp(b, t)
            out.append(a)
            seg = (b - a).length
            acc = 0.0
        acc += seg
    if (out[-1] - pts[-1]).length > step * 0.3:
        out.append(pts[-1])
    return out


def frames(pts):
    """평행 이동 프레임(접선, 법선, 종법선)."""
    T = []
    for i in range(len(pts)):
        a = pts[max(i - 1, 0)]
        b = pts[min(i + 1, len(pts) - 1)]
        T.append((b - a).normalized())
    ref = Vector((0, 0, 1)) if abs(T[0].z) < 0.9 else Vector((1, 0, 0))
    N = [(ref - T[0] * ref.dot(T[0])).normalized()]
    for i in range(1, len(pts)):
        n = N[-1] - T[i] * N[-1].dot(T[i])
        N.append(n.normalized())
    B = [t.cross(n) for t, n in zip(T, N)]
    return T, N, B


def sweep(name, path, section, material=None, cap=True):
    """path(Vector 목록)를 따라 단면 함수 section(i, angle)->(u, v) 를 쓸어 관을 만든다."""
    T, N, B = frames(path)
    bm = bmesh.new()
    rings = []
    k = len(section(0, 0.0, probe=True))
    for i, p in enumerate(path):
        sec = section(i, 0.0)
        rings.append([bm.verts.new(p + N[i] * u + B[i] * v) for u, v in sec])
    for r0, r1 in zip(rings, rings[1:]):
        for j in range(k):
            bm.faces.new((r0[j], r0[(j + 1) % k], r1[(j + 1) % k], r1[j]))
    if cap:
        bm.faces.new(list(reversed(rings[0])))
        bm.faces.new(rings[-1])
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    assign(ob, material)
    return smooth(ob)


def ellipse_section(a, b, k=20, power=2.6, taper=None):
    def f(i, _a, probe=False):
        s = 1.0 if taper is None or probe else taper(i)
        out = []
        for j in range(k):
            t = 2 * math.pi * j / k
            c, si = math.cos(t), math.sin(t)
            # 초타원(약간 각진 둥근 단면)
            u = a * s * math.copysign(abs(c) ** (2 / power), c)
            v = b * s * math.copysign(abs(si) ** (2 / power), si)
            out.append((u, v))
        return out
    return f


# ---------------------------------------------------------------- 본체 셸
Z0 = 0.005          # 셸 아랫면(바닥판 위)
TOP = 0.090         # 윗면 높이
PROFILE = [         # (y, z) 옆 단면: 앞(-Y)이 다이얼 경사면
    (-0.113, Z0), (0.110, Z0), (0.100, 0.068), (0.078, TOP),
    (-0.020, TOP), (-0.110, 0.020),
]


def half_w(z):
    return 0.105 - 0.24 * (z - Z0)


def body_shell():
    bm = bmesh.new()
    L = [bm.verts.new((-half_w(z), y, z)) for y, z in PROFILE]
    R = [bm.verts.new((half_w(z), y, z)) for y, z in PROFILE]
    n = len(PROFILE)
    bm.faces.new(L)
    bm.faces.new(list(reversed(R)))
    for i in range(n):
        bm.faces.new((L[i], L[(i + 1) % n], R[(i + 1) % n], R[i]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    # 모서리마다 둥글림 크기를 달리한다(베벨 가중치): 옆 모서리는 크게, 바닥은 각지게
    bw = bm.edges.layers.float.new('bevel_weight_edge')
    xw = [0.0, 0.0, 0.40, 0.55, 0.50, 0.28]          # 단면 꼭짓점(=X 방향 모서리)별
    for i in range(n):
        bm.edges.get((L[i], R[i]))[bw] = xw[i]
        for S in (L, R):
            bm.edges.get((S[i], S[(i + 1) % n]))[bw] = 0.0 if i == 0 else 1.0
    me = bpy.data.meshes.new('Body_Shell')
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new('Body_Shell', me)
    link(ob)
    assign(ob, RED)
    m = ob.modifiers.new('Bevel', 'BEVEL')
    m.width = 0.030
    m.segments = 10
    m.limit_method = 'WEIGHT'
    m.use_clamp_overlap = False
    m.profile = 0.55
    m.harden_normals = True      # 넓은 평면은 평평하게, 둥근 모서리만 부드럽게
    smooth(ob)
    apply_all(ob)
    return ob


body = body_shell()

# 바닥판: 셸보다 약간 안쪽으로 들어간 강철판
plate = box('Base_Plate', (0.200, 0.214, 0.0062), loc=(0, -0.0015, 0), material=BASE, base=True)
bevel(plate, 0.0018, 2)

# 고무 발 4개
FLOOR = -0.0015     # 고무 발 바닥 = 탁자면
foot = cylinder('Rubber_Foot', r=0.007, h=0.0015, loc=(0.085, 0.088, FLOOR), material=RUBBER, seg=20)
for sx, sy in ((-1, 1), (1, -1), (-1, -1)):
    duplicate(foot, loc=(0.085 * sx, 0.088 * sy, FLOOR))

# ---------------------------------------------------------------- 다이얼(경사면 위)
SLOPE_A = Vector((0, -0.020, TOP))
SLOPE_B = Vector((0, -0.110, 0.020))
slope_ang = math.atan2(SLOPE_A.z - SLOPE_B.z, SLOPE_A.y - SLOPE_B.y)
dial_c = (SLOPE_A + SLOPE_B) / 2
dial_root = bpy.data.objects.new('Dial_Assembly', None)
link(dial_root)
dial_root.location = dial_c
dial_root.rotation_euler = (slope_ang, 0, 0)


def on_dial(ob):
    ob.parent = dial_root
    return ob


R_PLATE = 0.0445
R_WHEEL = 0.0405
R_HOLE = 0.0300
HOLE_R = 0.0072

# 크롬 베젤 링(경사면에 살짝 묻힘)
on_dial(lathe('Dial_Bezel', [(R_PLATE - 0.001, -0.002), (R_PLATE + 0.0035, -0.002), (R_PLATE + 0.0042, 0.0012),
                             (R_PLATE + 0.0025, 0.0032), (R_PLATE - 0.0002, 0.0030), (R_PLATE - 0.001, -0.002)],
              material=CHROME, seg=72))
# 번호판(크림색 원판)
on_dial(cylinder('Dial_Number_Plate', r=R_PLATE, h=0.0032, loc=(0, 0, -0.001), material=CREAM, seg=72))

DIGITS = '1234567890'
for i, d in enumerate(DIGITS):
    a = math.radians(32 + 30 * i)       # 1 은 2시 방향, 반시계로 0 은 5시 방향
    t = text3d(f'Dial_Digit_{d}', d, size=0.0085, depth=0.00025,
               loc=(R_HOLE * math.cos(a), R_HOLE * math.sin(a), 0.00245), material=INK)
    t.data.align_y = 'CENTER'
    on_dial(to_mesh(t))

# 회전판(손가락 구멍 10개)
wheel = cylinder('Dial_Finger_Wheel', r=R_WHEEL, h=0.0042, loc=(0, 0, 0.0030), material=RED, seg=96)
holes = []
for i in range(10):
    a = math.radians(32 + 30 * i)
    holes.append(cylinder(f'hole_{i}', r=HOLE_R, h=0.02, loc=(R_HOLE * math.cos(a), R_HOLE * math.sin(a), -0.005), seg=32))
cutter = join(holes, 'Dial_Hole_Cutter')
boolean(wheel, cutter)
bevel(wheel, 0.0008, 2, 40)
apply_all(wheel)
bpy.data.objects.remove(cutter, do_unlink=True)
sharp_smooth(wheel, 40)
on_dial(wheel)

# 중앙 캡(크롬 테 + 크림 카드)
on_dial(lathe('Dial_Center_Cap', [(0, 0.0074), (0.0115, 0.0074), (0.0128, 0.0068), (0.0130, 0.0055), (0.0124, 0.0040),
                                  (0, 0.0040)], material=CHROME, seg=48))
on_dial(cylinder('Dial_Center_Card', r=0.0098, h=0.0004, loc=(0, 0, 0.0073), material=CREAM, seg=48))

for k, (w, yy) in enumerate(((0.012, 0.0028), (0.009, -0.0004), (0.006, -0.0032))):
    on_dial(box(f'Dial_Card_Print_{k}', (w, 0.0011, 0.0002), loc=(0, yy, 0.0078), material=INK))

# 손가락 멈춤쇠(4시 방향, 크롬 갈고리)
fs_a = math.radians(-32)
cdir = Vector((math.cos(fs_a), math.sin(fs_a), 0))
fs_pts = [cdir * (R_PLATE + 0.0005) + Vector((0, 0, 0.0018)), cdir * (R_PLATE + 0.0008) + Vector((0, 0, 0.0085)),
          cdir * (R_WHEEL - 0.0005) + Vector((0, 0, 0.0098)), cdir * (R_WHEEL - 0.0060) + Vector((0, 0, 0.0090))]
stop = curve_tube('Dial_Finger_Stop', [tuple(p) for p in fs_pts], radius=0.0016, material=CHROME, resolution=8)
on_dial(to_mesh(stop))

# ---------------------------------------------------------------- 받침(크래들) 뿔과 후크 버튼
HY = 0.040          # 수화기 중심선 y
HORN_X = 0.072
for sx in (-1, 1):
    side = 'L' if sx < 0 else 'R'
    for sy, tag in ((-1, 'Front'), (1, 'Back')):
        hb = box(f'Cradle_Horn_{side}_{tag}', (0.034, 0.013, 0.052),
                 loc=(sx * HORN_X, HY + sy * 0.0255, TOP - 0.020), material=RED, base=True)
        bevel(hb, 0.0062, 4, 30)
        apply_all(hb)
        sharp_smooth(hb, 40)
    pl = box(f'Hook_Plunger_{side}', (0.014, 0.016, 0.040), loc=(sx * HORN_X, HY, TOP - 0.004),
             material=PLUNGER, base=True)
    bevel(pl, 0.003, 3, 30)

# ---------------------------------------------------------------- 수화기
H_PATH = catmull([(-0.104, HY, 0.110), (-0.092, HY, 0.127), (-0.064, HY, 0.137), (0, HY, 0.139),
                  (0.064, HY, 0.137), (0.092, HY, 0.127), (0.104, HY, 0.110)], samples=10)
H_PATH = resample(H_PATH, 0.003)
handle = sweep('Handset_Handle', H_PATH, ellipse_section(0.0105, 0.0165, k=24), material=RED)
# (법선=위쪽 평면 방향 → u 는 두께, v 는 폭)

for sx, name in ((-1, 'Handset_Mouthpiece'), (1, 'Handset_Earpiece')):
    end = H_PATH[0] if sx < 0 else H_PATH[-1]
    prof = [(0.0, -0.032), (0.021, -0.032), (0.0262, -0.0306), (0.0285, -0.0275), (0.0292, -0.022),
            (0.0290, -0.014), (0.0278, -0.009), (0.0240, -0.004), (0.0170, 0.001), (0.0115, 0.006),
            (0.0, 0.008)]
    cup = lathe(name, prof, loc=end + Vector((sx * 0.004, 0, 0.004)), rot=(0, -sx * math.radians(24), 0),
                material=RED, seg=48)
    grille = cylinder(name + '_Grille', r=0.0185, h=0.0006, loc=(0, 0, -0.0323), material=GRILLE, seg=40)
    grille.parent = cup

# ---------------------------------------------------------------- 꼬인 수화기 코드
ZC = FLOOR + 0.0055 + 0.0016     # 바닥에 놓인 부분의 나선 중심 높이
cord_ctrl = [(-0.118, HY, 0.092), (-0.134, HY - 0.006, 0.070), (-0.148, HY - 0.020, 0.045),
             (-0.160, 0.000, 0.014), (-0.178, -0.045, ZC), (-0.208, -0.030, ZC), (-0.214, 0.030, ZC),
             (-0.190, 0.080, ZC), (-0.140, 0.092, ZC), (-0.112, 0.078, 0.012), (-0.098, 0.072, 0.016)]
cpath = resample(catmull(cord_ctrl, samples=20), 0.0004)
T, N, B = frames(cpath)
coil_r, pitch = 0.0055, 0.0042
helix = []
s = 0.0
for i, p in enumerate(cpath):
    if i:
        s += (cpath[i] - cpath[i - 1]).length
    ang = 2 * math.pi * s / pitch
    # 끝 부분은 나선 반지름을 줄여 곧은 코드로 이어지게
    fade = min(1.0, s / 0.012, (len(cpath) - 1 - i) * 0.0004 / 0.012)
    helix.append(p + (N[i] * math.cos(ang) + B[i] * math.sin(ang)) * coil_r * fade)
helix = helix[::2]
cord = curve_tube('Handset_Coiled_Cord', [tuple(p) for p in helix], radius=0.0016, material=RED,
                  resolution=1, bevel_res=1)
cord = to_mesh(cord)
smooth(cord)
grom = cylinder('Cord_Grommet_Side', r=0.0045, h=0.006, loc=(-0.097, 0.072, 0.016), rot=(0, -math.pi / 2, 0),
                material=RUBBER, seg=20)

# ---------------------------------------------------------------- 벽 연결선(뒤쪽)
line_pts = catmull([(0.040, 0.100, 0.014), (0.042, 0.118, 0.006), (0.050, 0.140, FLOOR + 0.0022),
                    (0.075, 0.175, FLOOR + 0.0022), (0.110, 0.190, FLOOR + 0.0022)], samples=10)
lc = to_mesh(curve_tube('Line_Cord', [tuple(p) for p in line_pts], radius=0.0022, material=RUBBER,
                        resolution=2, bevel_res=2))
smooth(lc)
plug = box('Line_Cord_Plug', (0.016, 0.011, 0.009), loc=(0.118, 0.193, FLOOR), rot=(0, 0, math.radians(20)),
           material=PLUNGER, base=True)
bevel(plug, 0.0015, 2)
