# 콘서트 그랜드 피아노 (길이 약 2.74m급, 뚜껑을 받침대로 연 연주 상태)
# - 날개 모양 케이스 윤곽은 제어점을 캐트멀-롬 곡선으로 보간해 얻고, 그 다각형을 안쪽으로 오프셋해
#   림(옆판) 벽, 향판, 철골 프레임, 뚜껑을 모두 같은 윤곽에서 만들었다.
# - 건반 88개(흰 건반 52 + 검은 건반 36)는 A0 부터 음이름 규칙으로 배열했고,
#   줄·튜닝핀·댐퍼는 메시를 공유하는 복제본을 수학적으로 배치했다(고음 3줄, 저음 동선 교차).
# - 다리·캐스터는 회전체(lathe), 리라와 페달은 상자·관으로 조립했다. 재질은 상수 색 Principled BSDF.
import math
from atelier import *

SHOT = {"azimuth": -48, "elevation": 26, "lens": 55, "light": "studio", "background": "#7d8894"}

# ---------------------------------------------------------------- 재질
EBONY = mat('Ebony_Lacquer', '#121215', rough=0.12, coat=1.0)
IVORY = mat('Key_Ivory', '#ece6d6', rough=0.3)
KEYBLK = mat('Key_Ebony', '#1b1a1c', rough=0.35)
GOLD = mat('Iron_Plate_Gold', '#c4923a', rough=0.4, metal=0.85)
SPRUCE = mat('Soundboard_Spruce', '#e6cb98', rough=0.5)
MAPLE = mat('Bridge_Maple', '#9c6b3f', rough=0.5)
STEEL = mat('String_Steel', '#d5d8dc', rough=0.18, metal=1.0)
COPPER = mat('Bass_String_Copper', '#b4703c', rough=0.3, metal=1.0)
BRASS = mat('Brass', '#c9a14c', rough=0.2, metal=1.0)
FELT_R = mat('Felt_Red', '#7e1b22', rough=0.9)
FELT_K = mat('Damper_Felt', '#2b2a2d', rough=0.85)

# ---------------------------------------------------------------- 치수
W = 1.57            # 케이스 폭
HW = W / 2
Z_BOT = 0.62        # 케이스 바닥(림 아래)
Z_TOP = 0.98        # 림 윗면
Y_FRONT = 0.30      # 림 앞벽 (건반 뒤)
RIM_T = 0.045       # 림 두께
CHEEK_X = 0.611     # 건반 양끝(치크 블록 안쪽 면)

# 벤트사이드(오른쪽 곡선 옆판)~꼬리 제어점 (x, y)
BENT = [(HW, 0.0), (HW, 0.55), (0.772, 0.78), (0.715, 1.00), (0.605, 1.22), (0.468, 1.45),
        (0.338, 1.68), (0.226, 1.92), (0.132, 2.15), (0.035, 2.38), (-0.10, 2.57),
        (-0.30, 2.70), (-0.52, 2.745), (-0.70, 2.73), (-HW, 2.67)]


def catmull(pts, n=10):
    out = []
    P = [pts[0]] + pts + [pts[-1]]
    for i in range(1, len(P) - 2):
        p0, p1, p2, p3 = P[i - 1], P[i], P[i + 1], P[i + 2]
        for k in range(n):
            t = k / n
            t2, t3 = t * t, t * t * t
            out.append(tuple(0.5 * ((2 * p1[j]) + (-p0[j] + p2[j]) * t + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * t2
                                    + (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * t3) for j in range(2)))
    out.append(pts[-1])
    return out


BENT_DENSE = catmull(BENT, 12)


def outline(front_y=Y_FRONT):
    """앞변이 front_y 인 반시계 방향 케이스 윤곽."""
    pts = [(-HW, front_y), (HW, front_y)]
    for x, y in BENT_DENSE:
        if y > front_y + 0.02:
            pts.append((x, y))
    pts.append((-HW, 2.55))
    pts.append((-HW, 1.5))
    pts.append((-HW, 0.9))
    return pts


def offset(pts, d):
    """반시계 다각형을 안쪽으로 d 만큼 (미터 접합)."""
    n = len(pts)
    out = []
    for i in range(n):
        p0, p1, p2 = Vector((*pts[i - 1], 0)), Vector((*pts[i], 0)), Vector((*pts[(i + 1) % n], 0))
        e1 = (p1 - p0).normalized()
        e2 = (p2 - p1).normalized()
        n1 = Vector((-e1.y, e1.x, 0))
        n2 = Vector((-e2.y, e2.x, 0))
        m = (n1 + n2).normalized()
        c = max(m.dot(n1), 0.3)
        q = p1 + m * (d / c)
        out.append((q.x, q.y))
    return out


def sharpen(ob, deg=35):
    smooth(ob)
    ob.data.set_sharp_from_angle(angle=math.radians(deg))
    return ob


def ring_wall(name, outer, inner, z0, z1, material):
    bm = bmesh.new()
    n = len(outer)
    ob_ = [bm.verts.new((x, y, z0)) for x, y in outer]
    ot = [bm.verts.new((x, y, z1)) for x, y in outer]
    ib = [bm.verts.new((x, y, z0)) for x, y in inner]
    it = [bm.verts.new((x, y, z1)) for x, y in inner]
    for i in range(n):
        j = (i + 1) % n
        bm.faces.new((ob_[i], ob_[j], ot[j], ot[i]))
        bm.faces.new((it[i], it[j], ib[j], ib[i]))
        bm.faces.new((ot[i], ot[j], it[j], it[i]))
        bm.faces.new((ib[i], ib[j], ob_[j], ob_[i]))
    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 sharpen(ob)


def slab(name, pts, z0, z1, material):
    ob = prism(name, pts, z1 - z0, loc=(0, 0, z0), material=material)
    return sharpen(ob)


def x_on_bent(y):
    """벤트사이드 바깥선의 y 에서의 x."""
    for (x0, y0), (x1, y1) in zip(BENT_DENSE, BENT_DENSE[1:]):
        if y0 <= y <= y1 and y1 > y0:
            return x0 + (x1 - x0) * (y - y0) / (y1 - y0)
    return -HW


def y_on_bent(x):
    """벤트사이드 바깥선이 x 를 지나는 y (꼬리 쪽 단조 구간)."""
    best = None
    for (x0, y0), (x1, y1) in zip(BENT_DENSE, BENT_DENSE[1:]):
        if min(x0, x1) <= x <= max(x0, x1) and x0 != x1:
            y = y0 + (y1 - y0) * (x - x0) / (x1 - x0)
            best = y if best is None else min(best, y)
    return best if best is not None else 2.7


OUT = outline()
INN = offset(OUT, RIM_T)

# ---------------------------------------------------------------- 케이스
rim = ring_wall('Case_Rim', OUT, INN, Z_BOT, Z_TOP, EBONY)
bottom_pts = offset([(-HW, -0.03), (HW, -0.03)] + OUT[2:], 0.008)
case_bottom = slab('Case_Bottom', bottom_pts, Z_BOT - 0.025, Z_BOT + 0.001, EBONY)
soundboard = slab('Soundboard', offset(OUT, RIM_T - 0.004), 0.785, 0.80, SPRUCE)
liner = ring_wall('Rim_Liner', offset(OUT, RIM_T - 0.003), offset(OUT, RIM_T + 0.035), 0.80, 0.83, EBONY)

# 치크 블록 (건반 양옆) — 측면 프로필을 X 방향으로 돌출
cheek_prof = [(-0.03, Z_BOT), (Y_FRONT, Z_BOT), (Y_FRONT, Z_TOP), (0.262, Z_TOP), (0.232, 0.972), (0.205, 0.952),
              (0.182, 0.92), (0.162, 0.876), (0.142, 0.836), (0.118, 0.812), (0.085, 0.802), (0.0, 0.80),
              (-0.022, 0.796), (-0.03, 0.785)]
cw = HW - CHEEK_X
for side, sx in (('L', -1), ('R', 1)):
    ck = extrude_profile(f'Cheek_Block_{side}', cheek_prof, cw, loc=(sx * (CHEEK_X + cw / 2), 0, 0),
                         material=EBONY, axis='X')
    sharpen(ck)

# ---------------------------------------------------------------- 건반부
key_frame = box('Key_Frame', (2 * CHEEK_X - 0.002, 0.215, 0.08), loc=(0, 0.0775, Z_BOT), material=EBONY, base=True)
key_slip = box('Key_Slip', (2 * CHEEK_X - 0.002, 0.022, 0.078), loc=(0, -0.011, Z_BOT + 0.014), material=EBONY, base=True)
felt_strip = box('Key_Felt_Strip', (2 * CHEEK_X - 0.004, 0.012, 0.004), loc=(0, 0.176, 0.722), material=FELT_R, base=True)

NW = 52
kw = 0.0235
kx0 = -NW * kw / 2
wkey = box('White_Key', (kw - 0.0013, 0.17, 0.022), loc=(0, 0.085, 0.70), material=IVORY, base=True)
bevel(wkey, 0.0015, 1)
bkey_proto = box('Black_Key', (0.0118, 0.095, 0.034), loc=(0, 0.1225, 0.701), material=KEYBLK, base=True)
bevel(bkey_proto, 0.002, 1)
names = 'ABCDEFG'
nb = 0
for i in range(NW):
    cx = kx0 + (i + 0.5) * kw
    k = wkey if i == 0 else duplicate(wkey, name=f'White_Key')
    k.location.x = cx
    if names[i % 7] in 'ACDFG' and i < NW - 1:
        b = bkey_proto if nb == 0 else duplicate(bkey_proto, name='Black_Key')
        b.location.x = kx0 + (i + 1) * kw
        nb += 1

# 건반 뚜껑(열린 상태로 세워진 폴보드)과 네임보드 블록
fallboard = box('Fallboard', (2 * CHEEK_X - 0.004, 0.03, 0.13), loc=(0, 0.198, 0.724), rot=(math.radians(-8), 0, 0),
                material=EBONY, base=True)
bevel(fallboard, 0.006, 2)
nameboard = box('Nameboard_Block', (2 * CHEEK_X - 0.002, 0.11, 0.25), loc=(0, 0.258, Z_BOT), material=EBONY, base=True)
logo = box('Fallboard_Inlay', (0.16, 0.004, 0.012), loc=(0, 0.2015, 0.80), rot=(math.radians(-8), 0, 0), material=GOLD)

# 보면대
desk_angle = math.radians(-14)
desk = box('Music_Desk', (0.86, 0.016, 0.30), loc=(0, 0.262, 0.868), rot=(desk_angle, 0, 0), material=EBONY, base=True)
bevel(desk, 0.004, 2)
desk_ledge = box('Music_Desk_Ledge', (0.86, 0.05, 0.016), loc=(0, 0.245, 0.869), material=EBONY, base=True)
bevel(desk_ledge, 0.003, 1)
desk_lip = box('Music_Desk_Lip', (0.86, 0.008, 0.026), loc=(0, 0.222, 0.869), material=EBONY, base=True)
desk_back = box('Music_Desk_Support', (0.05, 0.12, 0.012), loc=(0, 0.33, 0.866), rot=(math.radians(28), 0, 0), material=EBONY)

# ---------------------------------------------------------------- 철골 프레임(플레이트)
PZ0, PZ1 = 0.83, 0.85
plate = slab('Iron_Plate', offset(OUT, RIM_T + 0.012), PZ0, PZ1, GOLD)
open_pts = offset(outline(0.56), RIM_T + 0.13)
cut = prism('Plate_Opening_Cutter', open_pts, 0.1, loc=(0, 0, PZ0 - 0.03))
boolean(plate, cut)
# 앞쪽 바와 방사형 지지대
struts = [((-0.66, 0.60), (-0.55, 2.45)), ((-0.30, 0.60), (-0.38, 2.50)), ((0.05, 0.60), (-0.08, 2.32)),
          ((0.38, 0.60), (0.17, 1.80)), ((0.64, 0.60), (0.50, 1.20))]
for i, ((x0, y0), (x1, y1)) in enumerate(struts):
    L = math.hypot(x1 - x0, y1 - y0)
    ang = math.atan2(-(x1 - x0), y1 - y0)
    s = box(f'Plate_Strut_{i}', (0.06, L, 0.022), loc=((x0 + x1) / 2, (y0 + y1) / 2, PZ0 - 0.002),
            rot=(0, 0, ang), material=GOLD, base=True)
    bevel(s, 0.006, 2)
# 플레이트 장식 구멍(앞바의 둥근 구멍)
for i, x in enumerate([-0.52, -0.20, 0.12, 0.44]):
    cylinder(f'Plate_Hole_{i}', r=0.028, h=0.004, loc=(x, 0.545, PZ1 - 0.003), material=FELT_K, seg=24)

# ---------------------------------------------------------------- 줄, 핀, 브리지, 댐퍼
SZ = 0.858
unit = box('String_Unit', (1, 1, 1), material=STEEL)
unit_cu = box('String_Unit_Bass', (1, 1, 1), material=COPPER)
for u_ in (unit, unit_cu):
    for v_ in u_.data.vertices:
        v_.co.y += 0.5   # 줄 원점을 앞끝(핀 쪽)으로
pin = cylinder('Tuning_Pin', r=0.0035, h=0.03, loc=(0, 0, PZ1 - 0.002), material=STEEL, seg=8)
damper = box('Damper', (0.012, 0.035, 0.03), loc=(0, 0, SZ + 0.0015), material=FELT_K, base=True)
damper_top = box('Damper_Head', (0.010, 0.03, 0.012), loc=(0, 0, SZ + 0.031), material=MAPLE, base=True)
unit.hide_render = unit_cu.hide_render = True
unit.hide_viewport = unit_cu.hide_viewport = True
unit['atelier_cutter'] = unit_cu['atelier_cutter'] = True

Y_PIN = 0.40
first = True


def string(proto, name, p0, p1, z, w):
    dx, dy = p1[0] - p0[0], p1[1] - p0[1]
    L = math.hypot(dx, dy)
    s = duplicate(proto, name=name)
    s.hide_render = s.hide_viewport = False
    del s['atelier_cutter']
    s.location = (p0[0], p0[1], z)
    s.rotation_euler = (0, 0, math.atan2(-dx, dy))
    s.scale = (w, L, w)
    return s


bridge_pts = []
N_TREBLE = 66
tx0, tx1 = -0.40, 0.67
for c in range(N_TREBLE):
    t = c / (N_TREBLE - 1)
    cx = tx0 + (tx1 - tx0) * t
    yend = y_on_bent(cx + 0.06) - 0.16 if cx > -0.30 else 2.30 - (cx + 0.40) * 0.6
    yend = min(yend, 2.25)
    bridge_pts.append((cx, yend - 0.05))
    for k in (-1, 0, 1):
        x = cx + k * 0.0042
        string(unit, 'String_Steel', (x, Y_PIN + 0.01 * (k % 2)), (x, yend), SZ, 0.0016)
        p = duplicate(pin, name='Tuning_Pin', loc=(x, Y_PIN - 0.03 * (k % 2) - 0.015 * (k == 0), PZ1 - 0.002))
    if cx < 0.45:
        duplicate(damper, name='Damper', loc=(cx, 0.585, SZ + 0.0015))
        duplicate(damper_top, name='Damper_Head', loc=(cx, 0.585, SZ + 0.031))

N_BASS = 20
for c in range(N_BASS):
    t = c / (N_BASS - 1)
    fx = -0.735 + 0.30 * t
    rx = -0.62 + 0.52 * t
    ry = 2.42 - 0.08 * t
    for k in (0, 1):
        x = fx + k * 0.006
        string(unit_cu, 'String_Bass_Copper', (x, Y_PIN), (rx + k * 0.006, ry), SZ + 0.014, 0.0028)
        duplicate(pin, name='Tuning_Pin', loc=(x, Y_PIN - 0.02 * k, PZ1 - 0.002))
    # 댐퍼: 줄을 따라 y=0.585 지점
    u = (0.585 - Y_PIN) / (ry - Y_PIN)
    dxp = fx + 0.003 + (rx - fx) * u
    duplicate(damper, name='Damper', loc=(dxp, 0.585, SZ + 0.015))
    duplicate(damper_top, name='Damper_Head', loc=(dxp, 0.585, SZ + 0.045))
pin.hide_render = pin.hide_viewport = True
damper.hide_render = damper.hide_viewport = True
damper_top.hide_render = damper_top.hide_viewport = True
for o in (pin, damper, damper_top):
    o['atelier_cutter'] = True

# 브리지: 향판 위의 곡선 받침
bp = catmull(bridge_pts[::6] + [bridge_pts[-1]], 6)
for j, z in enumerate((0.81, 0.828, 0.846)):
    curve_tube(f'Treble_Bridge_{j}', [(x, y, z) for x, y in bp], radius=0.0115, material=MAPLE)
for j, z in enumerate((0.81, 0.83, 0.85, 0.858)):
    curve_tube(f'Bass_Bridge_{j}', [(-0.62 + 0.52 * t, 2.42 - 0.08 * t - 0.08, z) for t in (0, 0.5, 1)],
               radius=0.0125, material=MAPLE)

# 아그라프 바 (줄 앞단 받침)
box('Agraffe_Bar', (1.42, 0.02, 0.008), loc=(-0.02, Y_PIN + 0.03, PZ1), material=GOLD, base=True)

# ---------------------------------------------------------------- 뚜껑
LID_ANGLE = math.radians(39)
HX = -HW
lid_pts = [(x - HX, y) for x, y in offset(OUT, -0.004)]
lid = prism('Lid', lid_pts, 0.024, material=EBONY)
sharpen(lid)
bevel(lid, 0.004, 2)
flap = box('Lid_Front_Flap', (W - 0.01, 0.30, 0.02), loc=(W / 2, Y_FRONT + 0.15, 0.024), material=EBONY, base=True)
bevel(flap, 0.004, 2)
lid_pivot = bpy.data.objects.new('Lid_Hinge_Pivot', None)
link(lid_pivot)
lid_pivot.location = (HX, 0, Z_TOP)
lid_pivot.rotation_euler = (0, -LID_ANGLE, 0)
lid.parent = lid_pivot
flap.parent = lid_pivot
for i, y in enumerate((0.55, 1.45, 2.30)):
    cylinder(f'Lid_Hinge_{i}', r=0.008, h=0.09, loc=(HX - 0.002, y - 0.045, Z_TOP + 0.002), rot=(-math.pi / 2, 0, 0),
             material=BRASS, seg=12)

# 뚜껑 받침대: 벤트사이드 림 위 → 뚜껑 밑면
sb_y = 1.10
sb_x = x_on_bent(sb_y) - RIM_T / 2
d_top = 1.40
top = Vector((HX + d_top * math.cos(LID_ANGLE), sb_y, Z_TOP + d_top * math.sin(LID_ANGLE)))
base_p = Vector((sb_x, sb_y, Z_TOP))
v = top - base_p
stick = cylinder('Lid_Prop_Stick', r=0.013, h=v.length, loc=base_p, material=EBONY, seg=16, r2=0.011)
stick.rotation_euler = v.to_track_quat('Z', 'Y').to_euler()
smooth(stick)
cylinder('Prop_Stick_Cup', r=0.02, h=0.01, loc=base_p, material=BRASS, seg=16)

# ---------------------------------------------------------------- 다리와 캐스터
leg_prof = [(0.0, 0.035), (0.05, 0.035), (0.052, 0.045), (0.050, 0.10), (0.046, 0.11), (0.045, 0.12), (0.052, 0.13),
            (0.054, 0.14), (0.058, 0.30), (0.064, 0.46), (0.07, 0.49), (0.078, 0.50), (0.078, 0.53), (0.07, 0.54),
            (0.07, 0.56), (0.0, 0.56)]
legs_xy = [(-0.68, 0.14), (0.68, 0.14), (-0.36, 2.38)]
leg_proto = lathe('Leg', leg_prof, material=EBONY, seg=32)
sharpen(leg_proto, 40)
ferrule_proto = lathe('Leg_Ferrule', [(0.0, 0.036), (0.053, 0.036), (0.055, 0.045), (0.053, 0.115), (0.0, 0.115)],
                      material=BRASS, seg=32)
caster_wheel = cylinder('Caster_Wheel', r=0.034, h=0.022, loc=(0, -0.011, 0.034), rot=(-math.pi / 2, 0, 0),
                        material=BRASS, seg=24, base=False)
caster_wheel.location = (0, 0, 0.034)
caster_fork = box('Caster_Fork', (0.05, 0.03, 0.02), loc=(0, 0, 0.034), material=BRASS, base=True)
for i, (x, y) in enumerate(legs_xy):
    for proto in (leg_proto, ferrule_proto, caster_fork):
        o = proto if i == 0 else duplicate(proto)
        o.location.x, o.location.y = x, y
    cw_ = caster_wheel if i == 0 else duplicate(caster_wheel)
    cw_.location = (x, y, 0.034)
    blk = box(f'Leg_Block_{i}', (0.16, 0.16, 0.04), loc=(x, y, Z_BOT - 0.064), material=EBONY, base=True)
    bevel(blk, 0.01, 2)

# ---------------------------------------------------------------- 리라와 페달
LY = 0.20
pbox = box('Pedal_Box', (0.36, 0.13, 0.07), loc=(0, LY, 0.065), material=EBONY, base=True)
bevel(pbox, 0.01, 2)
for side, sx in (('L', -1), ('R', 1)):
    pts = [(sx * 0.13, LY, 0.13), (sx * 0.16, LY, 0.22), (sx * 0.15, LY, 0.33), (sx * 0.10, LY, 0.43),
           (sx * 0.085, LY, 0.52), (sx * 0.10, LY, 0.59)]
    curve_tube(f'Lyre_Arm_{side}', pts, radius=0.022, material=EBONY, resolution=8)
for i, x in enumerate((-0.035, 0.0, 0.035)):
    cylinder(f'Lyre_String_{i}', r=0.004, h=0.40, loc=(x, LY, 0.135), material=BRASS, seg=8)
box('Lyre_Top_Block', (0.30, 0.10, 0.04), loc=(0, LY, Z_BOT - 0.064), material=EBONY, base=True)
box('Lyre_Bottom_Rail', (0.30, 0.05, 0.02), loc=(0, LY, 0.133), material=EBONY, base=True)
for side, sx in (('L', -1), ('R', 1)):
    curve_tube(f'Lyre_Brace_{side}', [(sx * 0.15, LY + 0.06, 0.10), (sx * 0.20, 0.62, Z_BOT - 0.03)], radius=0.009,
               material=EBONY)
for i, x in enumerate((-0.085, 0.0, 0.085)):
    pd = box(f'Pedal_{i}', (0.035, 0.11, 0.012), loc=(x, LY - 0.11, 0.085), material=BRASS, base=True)
    bevel(pd, 0.004, 2)
    box(f'Pedal_Shank_{i}', (0.016, 0.06, 0.01), loc=(x, LY - 0.06, 0.086), material=BRASS, base=True)
