# 드레드넛 통기타(스틸 스트링 어쿠스틱 기타)와 접이식 기타 스탠드.
# 몸통 윤곽은 손으로 찍은 기준점을 캐트멀-롬 곡선으로 이어 만들고, 그 윤곽을 층층이 쌓아(로프트)
# 앞판·바인딩·옆판·뒤판을 하나의 메시에 면별 재질로 나눴다. 사운드홀은 불리언으로 뚫었다.
# 넥은 반타원 단면을 길이 방향으로 이어 만들었고, 프렛 위치는 평균율 공식 S·(1-2^(-n/12))로 계산했다.
# 헤드는 14° 뒤로 꺾인 판에 3+3 줄감개를 달았고, 여섯 줄은 브리지 핀-새들-너트-줄감개 기둥을 잇는 관이다.
# 기타는 로컬 좌표(앞판 +Z, 넥 +Y)에서 만든 뒤 빈 오브젝트에 묶어 75° 세워 스탠드에 기대 놓았다.
from atelier import *

SHOT = {"azimuth": -62, "elevation": 10, "lens": 60, "light": "studio", "background": "#25323b"}

# ---------------------------------------------------------------- 재질
M_TOP = mat('Spruce_Top', '#d8b27a', 0.38, coat=0.6)
M_BACK = mat('Rosewood_BackSides', '#4b2617', 0.32, coat=0.6)
M_NECK = mat('Mahogany_Neck', '#6e3a22', 0.4, coat=0.4)
M_EBONY = mat('Ebony_Fretboard', '#251a15', 0.45)
M_BIND = mat('Ivoroid_Binding', '#e6dbbf', 0.35)
M_HOLE = mat('Body_Interior', '#2e1c11', 0.8)
M_GUARD = mat('Tortoise_Pickguard', '#4a1a0c', 0.42)
M_FRET = mat('Nickel_Fretwire', '#cfc9bb', 0.22, 1.0)
M_CHROME = mat('Chrome_Tuners', '#d6d8da', 0.12, 1.0)
M_BRONZE = mat('Bronze_Strings', '#c09a5c', 0.3, 1.0)
M_STEEL = mat('Steel_Strings', '#c6c7c8', 0.2, 1.0)
M_PEARL = mat('Pearl_Inlay', '#ece6d8', 0.2)
M_STAND = mat('Stand_PowderCoat', '#2c2d30', 0.45, 0.6)
M_RUBBER = mat('Stand_Rubber', '#3a2f2b', 0.85)

# ---------------------------------------------------------------- 치수(미터)
BODY_L = 0.505                       # 몸통 길이
SCALE = 0.645                        # 줄 길이(너트~새들)
fret_d = lambda n: SCALE * (1 - 2 ** (-n / 12.0))
Y_NUT = BODY_L + fret_d(14)          # 14번 프렛이 몸통 끝과 만난다
Y_SADDLE = Y_NUT - SCALE
HOLE_C = (0.0, 0.362)
HOLE_R = 0.0505
FB_TOP = 0.006                        # 지판 윗면 높이
HEAD_ANG = math.radians(14)


def depth_at(y):
    """몸통 두께: 꼬리 쪽 12.2cm → 넥 쪽 10cm."""
    return 0.122 - 0.022 * min(max(y / BODY_L, 0), 1)


# ---------------------------------------------------------------- 공용 도구
def catmull(pts, n_per=24):
    """닫힌 캐트멀-롬 곡선 샘플링."""
    out = []
    N = len(pts)
    for i in range(N):
        p0, p1, p2, p3 = (Vector(pts[(i + k) % N]) for k in (-1, 0, 1, 2))
        for s in range(n_per):
            t = s / n_per
            t2, t3 = t * t, t * t * t
            out.append(0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3))
    return out


def resample(pts, n):
    """닫힌 다각형을 호 길이로 균등 재표본."""
    seg = [(pts[i] - pts[(i + 1) % len(pts)]).length for i in range(len(pts))]
    total = sum(seg)
    out, acc, i = [], 0.0, 0
    for k in range(n):
        target = total * k / n
        while acc + seg[i] < target:
            acc += seg[i]
            i += 1
        t = (target - acc) / seg[i]
        out.append(pts[i].lerp(pts[(i + 1) % len(pts)], t))
    return out


def loft(name, rings, mats, ring_mats, cap_start=None, cap_end=None, closed=True, smooth_all=True):
    """같은 점 개수의 고리들을 차례로 잇는다. ring_mats[i] = i번째와 i+1번째 고리 사이 면의 재질 번호."""
    bm = bmesh.new()
    vr = [[bm.verts.new(p) for p in r] for r in rings]
    n = len(rings[0])
    for i in range(len(rings) - 1):
        a, b = vr[i], vr[i + 1]
        for j in range(n if closed else n - 1):
            j2 = (j + 1) % n
            f = bm.faces.new((a[j], a[j2], b[j2], b[j]))
            f.material_index = ring_mats[i]
            f.smooth = smooth_all
    if cap_start is not None:
        f = bm.faces.new(list(reversed(vr[0])))
        f.material_index = cap_start
    if cap_end is not None:
        f = bm.faces.new(vr[-1])
        f.material_index = cap_end
    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)
    for m in mats:
        me.materials.append(m)
    return ob


def annulus(name, r1, r2, z0, z1, material, seg=96):
    return lathe(name, [(r1, z0), (r2, z0), (r2, z1), (r1, z1), (r1, z0)], material=material, seg=seg, shade_smooth=False)


def tube(name, points, radius, material, bevel_res=3):
    """curve_tube 를 만든 뒤 바로 메시로 바꾼다(경계상자·내보내기 안정)."""
    cu = curve_tube(name, points, radius=radius, material=material, resolution=1, bevel_res=bevel_res)
    dg = bpy.context.evaluated_depsgraph_get()
    me = bpy.data.meshes.new_from_object(cu.evaluated_get(dg))
    bpy.data.objects.remove(cu, do_unlink=True)
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return smooth(ob)


GUITAR_PARTS = []


def G(ob):
    GUITAR_PARTS.append(ob)
    return ob


# ================================================================ 몸통
# 오른쪽 절반 기준점(x=반폭, y=꼬리에서의 거리). 아래 볼 40cm, 허리 27cm, 위 볼 29cm.
half = [(0.0, 0.0), (0.075, 0.004), (0.135, 0.022), (0.176, 0.052), (0.197, 0.098), (0.199, 0.145),
        (0.188, 0.195), (0.165, 0.243), (0.142, 0.285), (0.137, 0.312), (0.141, 0.345), (0.146, 0.385),
        (0.146, 0.425), (0.139, 0.462), (0.118, 0.488), (0.075, 0.503), (0.0, BODY_L)]
key = half + [(-x, y) for x, y in reversed(half[1:-1])]
outline = resample(catmull([Vector((x, y, 0)) for x, y in key], 20), 192)
N = len(outline)
norms = []
for i in range(N):
    t = (outline[(i + 1) % N] - outline[i - 1]).normalized()
    norms.append(Vector((-t.y, t.x, 0)))       # 반시계 윤곽의 안쪽 법선


def ring_at(zfn, inset):
    return [Vector((p.x + nrm.x * inset, p.y + nrm.y * inset, zfn(p.y))) for p, nrm in zip(outline, norms)]


R = 0.0024
layers = [
    (lambda y: 0.0, R), (lambda y: -0.0007, 0.0007), (lambda y: -R, 0.0),
    (lambda y: -0.0065, 0.0),
    (lambda y: -depth_at(y) + 0.0065, 0.0),
    (lambda y: -depth_at(y) + R, 0.0), (lambda y: -depth_at(y) + 0.0007, 0.0007), (lambda y: -depth_at(y), R),
]
rings = [ring_at(z, ins) for z, ins in layers]
# 재질 번호: 0 앞판, 1 바인딩, 2 옆·뒤판, 3 속(사운드홀)
body = loft('Guitar_Body', rings, [M_TOP, M_BIND, M_BACK, M_HOLE], [1, 1, 1, 2, 1, 1, 1], cap_start=0, cap_end=2)
for p in body.data.polygons:
    if p.material_index in (0, 2) and abs(p.normal.z) > 0.9:
        p.use_smooth = False
hole_cut = cylinder('SoundHole_Cutter', HOLE_R, 0.105, loc=(HOLE_C[0], HOLE_C[1], -0.095), material=M_HOLE, seg=64)
boolean(body, hole_cut)
body.modifiers[-1].material_mode = 'TRANSFER'
apply_all(body)
G(body)

# 로제트(사운드홀 둘레 상감 고리)
G(annulus('Rosette_Band', 0.0565, 0.0635, -0.0004, 0.0002, M_BACK))
G(annulus('Rosette_Ring_Inner', 0.0555, 0.0575, -0.0004, 0.0004, M_BIND))
G(annulus('Rosette_Ring_Outer', 0.0625, 0.0645, -0.0004, 0.0004, M_BIND))
G(annulus('Rosette_Ring_Outer2', 0.0660, 0.0668, -0.0004, 0.0003, M_EBONY))
G(annulus('SoundHole_Edge', HOLE_R - 0.0001, HOLE_R + 0.0012, -0.0035, 0.0001, M_TOP))
for ob in GUITAR_PARTS[-5:]:
    ob.location = (HOLE_C[0], HOLE_C[1], 0)

# 앞판 가장자리 퍼플링(검은 줄)
pur = loft('Top_Purfling', [ring_at(lambda y: -0.0004, 0.0042), ring_at(lambda y: 0.00025, 0.0042),
                            ring_at(lambda y: 0.00025, 0.0058), ring_at(lambda y: -0.0004, 0.0058)],
           [M_EBONY], [0, 0, 0], smooth_all=False)
G(pur)

# 뒤판 가운데 띠(백 스트립)
_sl = math.atan(0.022 / BODY_L)
G(box('Back_Center_Strip', (0.0055, 0.484, 0.0012), loc=(0, 0.2535, -depth_at(0.2535)), rot=(_sl, 0, 0), material=M_BIND))

# 픽가드(물방울형, 사운드홀 오른쪽 아래를 감싼다)
pg = []
A0, A1 = math.radians(-118), math.radians(28)
K = 40
for k in range(K + 1):
    a = A0 + (A1 - A0) * k / K
    pg.append((HOLE_C[0] + 0.0688 * math.cos(a), HOLE_C[1] + 0.0688 * math.sin(a)))
for k in range(K, -1, -1):
    u = k / K
    a = A0 + (A1 - A0) * u
    w = 0.064 * math.sin(math.pi * (u ** 0.85)) ** 0.55 * (1 - 0.35 * u)
    r = 0.0688 + w
    pg.append((HOLE_C[0] + r * math.cos(a), HOLE_C[1] + r * math.sin(a)))
G(prism('Pickguard', pg[:-1], 0.0007, loc=(0, 0, -0.0003), material=M_GUARD))

# 브리지(배불뚝이형)와 새들, 브리지 핀
bp = []
BL, BW = 0.076, 0.0155
for k in range(25):
    t = -1 + 2 * k / 24
    bp.append((BL * t, -BW - 0.004 * math.cos(t * math.pi / 2) ** 2))
for k in range(25):
    t = 1 - 2 * k / 24
    bp.append((BL * t * 0.985, BW))
bridge = prism('Bridge', bp, 0.0095, loc=(0, Y_SADDLE - 0.006, -0.0005), material=M_EBONY)
bevel(bridge, 0.0025, 3, 40)
G(bridge)
saddle = box('Saddle', (0.074, 0.0028, 0.0055), loc=(0, Y_SADDLE, 0.0074), rot=(0, 0, math.radians(1.5)), material=M_BIND, base=True)
bevel(saddle, 0.0009, 2, 40)
G(saddle)
Y_PIN = Y_SADDLE - 0.0135
pin = join([cylinder('Pin_shaft', 0.0026, 0.004, loc=(0, 0, 0.0075), material=M_BIND, seg=16),
            sphere('Pin_head', 0.0034, loc=(0, 0, 0.0108), material=M_BIND, seg=16, rings=8, scale=(1, 1, 0.55))], 'Bridge_Pin_1')
G(pin)
STR_X_SADDLE = [-0.0262 + 0.0524 * i / 5 for i in range(6)]
STR_X_NUT = [-0.0178 + 0.0356 * i / 5 for i in range(6)]
for i, x in enumerate(STR_X_SADDLE):
    if i == 0:
        pin.location = (x, Y_PIN, 0.0075)
    else:
        G(duplicate(pin, loc=(x, Y_PIN, 0.0075), name=f'Bridge_Pin_{i + 1}'))

# 꼬리 스트랩 버튼
G(join([cylinder('EndPin_base', 0.0055, 0.004, rot=(math.pi / 2, 0, 0), loc=(0, 0.0015, -0.06), material=M_BIND, seg=20),
        cylinder('EndPin_knob', 0.0045, 0.008, r2=0.0035, rot=(math.pi / 2, 0, 0), loc=(0, -0.002, -0.06), material=M_BIND, seg=20)],
       'End_Pin'))

# ================================================================ 넥
FB_END = 0.4175


def fb_half(y):
    return 0.5 * (0.0435 + (Y_NUT - y) * (0.0125 / fret_d(14)))


def neck_depth(y):
    base = 0.0212 + 0.0028 * min(max((Y_NUT - y) / (Y_NUT - 0.57), 0), 1)
    t = min(max((0.562 - y) / (0.562 - 0.506), 0), 1)
    heel = t * t * (3 - 2 * t)
    return base + (depth_at(0.5) - 0.002 - base) * heel


nrings = []
ys = [0.497 + (Y_NUT + 0.028 - 0.497) * k / 60 for k in range(61)]
for y in ys:
    hw = fb_half(min(y, Y_NUT))
    d = neck_depth(min(y, Y_NUT))
    ztop = 0.0
    if y > Y_NUT:
        u = (y - Y_NUT) / 0.028
        ztop = -(y - Y_NUT) * math.tan(HEAD_ANG) - 0.0016 * min(u * 4, 1)
        d = d + (0.0152 - d) * u
    if y < 0.575:
        hw *= 0.93 + 0.07 * (y - 0.497) / 0.078
    ring = []
    for j in range(19):
        a = math.pi * j / 18
        s = math.sin(a)
        ring.append(Vector((hw * math.cos(a), y, ztop - d * s ** 0.85)))
    nrings.append(ring)
neck = loft('Neck', nrings, [M_NECK], [0] * 60, cap_start=0, cap_end=0, closed=True)
G(neck)

# 지판
fbp = [(-fb_half(FB_END), FB_END), (fb_half(FB_END), FB_END), (fb_half(Y_NUT), Y_NUT), (-fb_half(Y_NUT), Y_NUT)]
G(prism('Fretboard', fbp, FB_TOP + 0.0005, loc=(0, 0, -0.0005), material=M_EBONY))

# 프렛 20개(같은 메시를 폭만 늘려 공유)
fret = cylinder('Fret_01', 0.0011, 1.0, rot=(0, math.pi / 2, 0), loc=(-0.5, 0, 0), material=M_FRET, seg=10, base=True)
G(fret)
for n in range(1, 21):
    y = Y_NUT - fret_d(n)
    w = 2 * fb_half(y) - 0.0004
    ob = fret if n == 1 else G(duplicate(fret, name=f'Fret_{n:02d}'))
    ob.location = (-w / 2, y, FB_TOP - 0.0002)
    ob.scale = (1, 1, w)

# 포지션 마크(자개 점)
dot = cylinder('Inlay_Dot', 0.0032, 0.0008, loc=(0, 0, FB_TOP - 0.0007), material=M_PEARL, seg=20)
G(dot)
first = True
for n in (3, 5, 7, 9, 12, 15, 17):
    y = Y_NUT - 0.5 * (fret_d(n) + fret_d(n - 1))
    for x in ((-0.0075, 0.0075) if n == 12 else (0.0,)):
        if first:
            dot.location = (x, y, FB_TOP - 0.0007)
            first = False
        else:
            G(duplicate(dot, loc=(x, y, FB_TOP - 0.0007), name=f'Inlay_Dot_{n}'))

# 너트
nut = box('Nut', (2 * fb_half(Y_NUT), 0.005, 0.0088), loc=(0, Y_NUT + 0.0025, -0.0005), material=M_BIND, base=True)
bevel(nut, 0.0008, 2, 40)
G(nut)

# ================================================================ 헤드
HEAD_L = 0.19
head_m = Matrix.Translation((0, Y_NUT, 0)) @ Matrix.Rotation(-HEAD_ANG, 4, 'X')
hp = [(-0.0375, 0.0), (0.0375, 0.0), (0.0435, HEAD_L - 0.012), (0.04, HEAD_L), (-0.04, HEAD_L), (-0.0435, HEAD_L - 0.012)]
bmh = bmesh.new()
bot = [bmh.verts.new((x, y, -0.0155)) for x, y in hp]
topv = [bmh.verts.new((x, y, 0.0)) for x, y in hp]
bmh.faces.new(list(reversed(bot))).material_index = 0
bmh.faces.new(topv).material_index = 1
for i in range(len(hp)):
    bmh.faces.new((bot[i], bot[(i + 1) % len(hp)], topv[(i + 1) % len(hp)], topv[i])).material_index = 0
bmesh.ops.recalc_face_normals(bmh, faces=bmh.faces)
me = bpy.data.meshes.new('Headstock')
bmh.to_mesh(me)
bmh.free()
me.materials.append(M_NECK)
me.materials.append(M_EBONY)
headstock = bpy.data.objects.new('Headstock', me)
link(headstock)
headstock.matrix_world = head_m
bevel(headstock, 0.0018, 2, 40)
G(headstock)

# 줄감개 3+3 (기둥은 앞면, 기어 상자와 손잡이는 뒷면 바깥쪽)
POST_Y = [0.045, 0.087, 0.129]
POST_X = 0.0255
tuner_parts = [
    cylinder('Tuner_bushing', 0.0048, 0.0022, loc=(0, 0, -0.0004), material=M_CHROME, seg=20),
    cylinder('Tuner_post', 0.003, 0.0145, loc=(0, 0, 0), material=M_CHROME, seg=16),
    box('Tuner_gearbox', (0.017, 0.016, 0.0105), loc=(0.004, 0, -0.0155 - 0.0105 + 0.0004), material=M_CHROME, base=True),
    cylinder('Tuner_shaft', 0.0018, 0.022, rot=(0, math.pi / 2, 0), loc=(0.010, 0, -0.0205), material=M_CHROME, seg=12),
    sphere('Tuner_button', 0.0105, loc=(0.035, 0, -0.0205), material=M_CHROME, seg=24, rings=12, scale=(0.72, 0.28, 0.95)),
]
bevel(tuner_parts[2], 0.002, 2, 40)
tuner = join(tuner_parts, 'Tuner_Treble_1')
smooth(tuner)
POSTS_WORLD = []
k = 0
for side in (-1, 1):            # -1 = 저음 쪽(관람자 왼쪽)
    for j, py in enumerate(POST_Y):
        ob = tuner if k == 0 else duplicate(tuner)
        ob.name = f"Tuner_{'Bass' if side < 0 else 'Treble'}_{j + 1}"
        ob.matrix_world = head_m @ Matrix.Translation((side * POST_X, py, 0)) @ Matrix.Diagonal((side, 1, 1, 1))
        G(ob)
        POSTS_WORLD.append(head_m @ Vector((side * POST_X, py, 0.0105)))
        k += 1

# ================================================================ 줄 6개
# 줄 순서: 0=6번(저음 E) … 5=1번(고음 E). 저음 3줄은 왼쪽 기둥, 고음 3줄은 오른쪽 기둥.
post_for = [POSTS_WORLD[2], POSTS_WORLD[1], POSTS_WORLD[0], POSTS_WORLD[3], POSTS_WORLD[4], POSTS_WORLD[5]]
radii = [0.00068, 0.00058, 0.00048, 0.00040, 0.00030, 0.00025]
for i in range(6):
    xs, xn = STR_X_SADDLE[i], STR_X_NUT[i]
    post = post_for[i]
    pts = [(xs, Y_PIN, 0.0100), (xs, Y_SADDLE - 0.0006, FB_TOP + 0.0065 + 0.0000),
           (xs, Y_SADDLE + 0.0006, FB_TOP + 0.0068),
           (xn, Y_NUT + 0.0012, FB_TOP + 0.0029), (xn, Y_NUT + 0.0045, FB_TOP + 0.0022),
           (post.x - 0.0028 * (1 if post.x > 0 else -1) * 0, post.y - 0.0, post.z)]
    st = tube(f'String_{6 - i}', pts, radii[i], M_BRONZE if i < 4 else M_STEEL, bevel_res=1)
    G(st)

# ================================================================ 세우기
pivot = bpy.data.objects.new('Guitar_Dreadnought', None)
link(pivot)
TILT = math.radians(75)
pivot.matrix_world = Matrix.Translation((0, 0, 0.075)) @ Matrix.Rotation(TILT, 4, 'X')
GM = pivot.matrix_world.copy()
for ob in GUITAR_PARTS:
    ob.parent = pivot


def W(x, y, z):
    return tuple(GM @ Vector((x, y, z)))


# ================================================================ 스탠드
TR = 0.0085
HUB = Vector((0, 0.34, 0.13))
# 받침 팔: 몸통 꼬리 쪽 아래 면을 따라가다 앞에서 위로 꺾인다
for side in (-1, 1):
    x = side * 0.105
    # 이 x 에서 몸통 윤곽의 꼬리쪽 y
    yb = min(p.y for p in outline if abs(p.x - x) < 0.006 and p.y < 0.1)
    off = TR + 0.0035
    front = Vector(W(x, yb - off, 0.012))
    under_a = Vector(W(x, yb - off, -0.004))
    under_b = Vector(W(x, yb - off, -depth_at(yb) + 0.006))
    back = Vector(W(x, yb - off - 0.01, -depth_at(yb) - 0.02))
    pad = tube(f'Stand_Cradle_Pad_{"L" if side < 0 else "R"}',
                     [front + Vector((0, 0, 0.03)), front, under_a, under_b], TR + 0.002, M_RUBBER)
    arm = tube(f'Stand_Cradle_Arm_{"L" if side < 0 else "R"}',
                     [under_b + (under_b - under_a).normalized() * -0.002, back, HUB + Vector((side * 0.01, 0, 0))],
                     TR, M_STAND)
# 목 받침(U자 요크) - 기타 좌표에서 만든다
YOKE_Y = 0.80
uk = []
for k in range(17):
    a = math.pi * k / 16
    uk.append(W(0.031 * math.cos(a), YOKE_Y, 0.004 - 0.0305 * math.sin(a)))
yoke = tube('Stand_Neck_Yoke', uk, 0.0055, M_RUBBER)
yoke_bottom = Vector(W(0, YOKE_Y, 0.004 - 0.0305 - 0.004))
tube('Stand_Mast', [yoke_bottom, yoke_bottom + Vector((0, 0.012, -0.03)), HUB], TR, M_STAND)
# 다리 셋과 고무발
feet = [Vector((-0.2, 0.06, 0.0)), Vector((0.2, 0.06, 0.0)), Vector((0, 0.58, 0.0))]
for i, f in enumerate(feet):
    tube(f'Stand_Leg_{i + 1}', [HUB, f + Vector((0, 0, TR + 0.006))], TR, M_STAND)
    sphere(f'Stand_Foot_{i + 1}', 0.016, loc=f + Vector((0, 0, 0.012)), material=M_RUBBER, seg=20, rings=10, scale=(1, 1, 0.75))
sphere('Stand_Hub', 0.02, loc=HUB, material=M_STAND, seg=24, rings=12)
