# 모카포트 (비알레띠 모카 익스프레스형, 6컵) — 실제 크기(높이 약 22cm)로 코드만으로 만든 모델.
# 아래 보일러와 위 커피통은 '팔각 회전체'다: (반폭, 높이) 측면 프로필을 8각으로 돌려 면이 각진 알루미늄 몸통을 만들고,
# 베벨 모디파이어로 모서리마다 가는 하이라이트가 서도록 했다. 두 몸통이 만나 잘록한 허리(나사 이음 칼라)가 생긴다.
# 손잡이는 닫힌 중심선을 따라 두께·폭이 변하는 사각 단면 고리를 bmesh 로 짠 뒤 서브서프로 둥글린 검은 수지 부품,
# 뚜껑은 팔각 낮은 피라미드에 검은 꼭지, 앞쪽에는 뾰족한 주둥이, 보일러 옆면에는 육각 안전밸브를 달았다.
# 내부(깔때기 필터, 가스켓, 커피 관)는 뚜껑을 닫은 상태라 보이지 않으므로 만들지 않았다.
from atelier import *

SHOT = {"azimuth": -58, "elevation": 18, "lens": 70, "light": "studio", "background": "#2b2724"}

ALU = mat('Aluminium_Body', '#aeb1b3', rough=0.30, metal=1.0)
ALU_CAST = mat('Aluminium_Cast', '#9c9e9e', rough=0.46, metal=1.0)
STEEL = mat('Steel_Valve', '#cfccc6', rough=0.22, metal=1.0)
RESIN = mat('Black_Resin', '#1f1d1c', rough=0.42)
BADGE = mat('Badge_Enamel', '#5c1f1b', rough=0.40)

COS8 = math.cos(math.pi / 8)          # 팔각형: 꼭짓점 반경 = 반폭 / cos(22.5°)
FACET0 = math.pi / 8                   # 면이 ±X, ±Y 를 향하도록 꼭짓점을 22.5° 돌린다


def oct_lathe(name, profile, material, cap_bottom=False, cap_top=False, bevel_w=0.0008):
    """profile=[(반폭 a, z), ...] 를 팔각으로 돌린 각진 회전체."""
    bm = bmesh.new()
    rings = []
    for a, z in profile:
        R = a / COS8
        rings.append([bm.verts.new((R * math.cos(FACET0 + k * math.pi / 4),
                                    R * math.sin(FACET0 + k * math.pi / 4), z)) for k in range(8)])
    for j in range(len(rings) - 1):
        for k in range(8):
            bm.faces.new((rings[j][k], rings[j][(k + 1) % 8], rings[j + 1][(k + 1) % 8], rings[j + 1][k]))
    if cap_bottom:
        bm.faces.new(list(reversed(rings[0])))
    if cap_top:
        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)
    if bevel_w:
        m = ob.modifiers.new('EdgeBevel', 'BEVEL')
        m.width = bevel_w
        m.segments = 3
        m.limit_method = 'ANGLE'
        m.angle_limit = math.radians(20)
        m.harden_normals = True
    return ob


def apothem_at(profile, z):
    """프로필에서 높이 z 의 반폭(선형 보간)."""
    for (a0, z0), (a1, z1) in zip(profile, profile[1:]):
        if z0 <= z <= z1 and z1 > z0:
            t = (z - z0) / (z1 - z0)
            return a0 + (a1 - a0) * t
    return profile[-1][0]


# ---------------------------------------------------------------- 보일러(아래 물통)
BOILER = [
    (0.0515, 0.0000), (0.0540, 0.0020), (0.0545, 0.0060),   # 바닥 굽
    (0.0532, 0.0085), (0.0512, 0.0200), (0.0478, 0.0380),
    (0.0436, 0.0560), (0.0398, 0.0700), (0.0378, 0.0770),
    (0.0372, 0.0800),
]
oct_lathe('Boiler', BOILER, ALU, cap_bottom=True)

# ---------------------------------------------------------------- 커피통(위 몸통) + 나사 이음 칼라
COLLAR = [
    (0.0380, 0.0800), (0.0398, 0.0810), (0.0408, 0.0880),
    (0.0412, 0.0895), (0.0396, 0.0910),
]
oct_lathe('Collector_Collar', COLLAR, ALU_CAST, cap_bottom=True)

COLLECTOR = [
    (0.0380, 0.0910), (0.0382, 0.0950), (0.0408, 0.1200),
    (0.0443, 0.1450), (0.0480, 0.1680), (0.0496, 0.1750),
    (0.0512, 0.1760), (0.0516, 0.1785), (0.0490, 0.1790),   # 말린 테두리
]
oct_lathe('Collector', COLLECTOR, ALU)

# ---------------------------------------------------------------- 뚜껑 + 꼭지
LID_Z = 0.1790
LID = [
    (0.0508, LID_Z), (0.0512, LID_Z + 0.0015), (0.0508, LID_Z + 0.0035),
    (0.0420, LID_Z + 0.0100), (0.0300, LID_Z + 0.0165), (0.0170, LID_Z + 0.0205),
    (0.0120, LID_Z + 0.0212),
]
oct_lathe('Lid', LID, ALU, cap_top=True)
LID_TOP = LID_Z + 0.0212

knob = lathe('Lid_Knob', [
    (0.0001, LID_TOP - 0.0005), (0.0095, LID_TOP - 0.0005), (0.0090, LID_TOP + 0.0015),
    (0.0062, LID_TOP + 0.0045), (0.0068, LID_TOP + 0.0070), (0.0105, LID_TOP + 0.0110),
    (0.0112, LID_TOP + 0.0140), (0.0098, LID_TOP + 0.0170), (0.0060, LID_TOP + 0.0190),
    (0.0001, LID_TOP + 0.0196),
], material=RESIN, seg=64)

# ---------------------------------------------------------------- 주둥이(+X 면)
def spout():
    """윗 테두리에서 앞으로 뻗은 뾰족한 주둥이. 용골선이 벽에서 나와 위로 휘어 나간다."""
    a_top = 0.0512
    z_top = 0.1785
    z_low = z_top - 0.050
    bm = bmesh.new()
    tip = bm.verts.new((a_top + 0.0235, 0, z_top + 0.0045))
    l = bm.verts.new((a_top - 0.004, -0.0215, z_top))
    r = bm.verts.new((a_top - 0.004, 0.0215, z_top))
    l2 = bm.verts.new((a_top - 0.004, -0.0205, z_top - 0.0028))
    r2 = bm.verts.new((a_top - 0.004, 0.0205, z_top - 0.0028))
    keel = []                                  # 뱃머리 용골선(아래 벽 → 위 끝)
    N = 12
    for i in range(N + 1):
        s = i / N
        z = z_low + (z_top + 0.0012 - z_low) * s
        x = apothem_at(COLLECTOR, z) - 0.0006 + (a_top + 0.0221 - apothem_at(COLLECTOR, z_top)) * s ** 2.2
        keel.append(bm.verts.new((x, 0, z)))
    tip2 = keel[-1]
    bm.faces.new((tip, r, l))                 # 윗면(뚜껑 앞자락)
    bm.faces.new((tip, l, l2, tip2))
    bm.faces.new((tip, tip2, r2, r))
    for i in range(N):                         # 용골에서 양옆 모서리로 펼쳐지는 볼록면
        bm.faces.new((keel[i + 1], l2, keel[i]))
        bm.faces.new((keel[i], r2, keel[i + 1]))
    bm.faces.new([l, r, r2, keel[0], l2])
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new('Spout')
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new('Spout', me)
    link(ob)
    assign(ob, ALU)
    m = ob.modifiers.new('EdgeBevel', 'BEVEL')
    m.width = 0.0007
    m.segments = 2
    m.limit_method = 'ANGLE'
    m.harden_normals = True
    return ob


spout()

# ---------------------------------------------------------------- 손잡이(-X 면, 검은 수지)
def catmull_closed(pts, n):
    out = []
    m = len(pts)
    for i in range(m):
        p0, p1, p2, p3 = (Vector(pts[(i + k) % m]) for k in (-1, 0, 1, 2))
        for s in range(n):
            t = s / n
            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 handle():
    # (벽에서 바깥으로 거리 u, 높이 z, 고리 폭 w, 두께 t)
    ctrl = [
        (0.004, 0.1700, 0.014, 0.017), (0.028, 0.1765, 0.014, 0.018), (0.052, 0.1750, 0.013, 0.017),
        (0.061, 0.1640, 0.012, 0.016), (0.056, 0.1400, 0.012, 0.015), (0.043, 0.1150, 0.011, 0.014),
        (0.026, 0.1030, 0.010, 0.013), (0.011, 0.1080, 0.009, 0.012), (0.005, 0.1400, 0.009, 0.012),
    ]
    samples = catmull_closed([(u, z, w, t) for u, z, w, t in ctrl], 16)
    n = len(samples)
    bm = bmesh.new()
    sec = []
    for i, s in enumerate(samples):
        u, z, w, t = s
        nxt, prv = samples[(i + 1) % n], samples[i - 1]
        du, dz = nxt[0] - prv[0], nxt[1] - prv[1]
        L = math.hypot(du, dz) or 1
        nu, nz = dz / L, -du / L          # 진행 방향의 법선(평면 안)
        x_wall = -apothem_at(COLLECTOR, z)
        quad = []
        for side, half in ((1, w * 0.75), (-1, w * 0.75)):
            for yy in (-t * 0.75, t * 0.75):
                uu = u + side * nu * half
                zz = z + side * nz * half
                quad.append(bm.verts.new((x_wall - uu, yy, zz)))
        sec.append([quad[0], quad[1], quad[3], quad[2]])
    for i in range(n):
        a, b = sec[i], sec[(i + 1) % n]
        for k in range(4):
            bm.faces.new((a[k], a[(k + 1) % 4], b[(k + 1) % 4], b[k]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new('Handle')
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new('Handle', me)
    link(ob)
    assign(ob, RESIN)
    subsurf(ob, 2)
    smooth(ob)
    return ob


handle()

# 손잡이를 몸통에 고정하는 알루미늄 브래킷과 뚜껑 경첩
a_br = apothem_at(COLLECTOR, 0.166)
box('Handle_Bracket', (0.016, 0.020, 0.012), loc=(-a_br - 0.004, 0, 0.160), material=ALU_CAST, base=True)
hinge_x = -0.0512 - 0.006
box('Lid_Hinge_Tab', (0.014, 0.012, 0.004), loc=(hinge_x + 0.002, 0, LID_Z + 0.0005), material=ALU, base=True)
# 엄지로 눌러 뚜껑을 여는 꼬리(뚜껑 뒤쪽에서 비스듬히 솟음)
box('Lid_Thumb_Tab', (0.016, 0.014, 0.003), loc=(hinge_x - 0.005, 0, LID_Z + 0.0050),
    rot=(0, math.radians(28), 0), material=ALU, base=True)
cylinder('Lid_Hinge_Pin', r=0.0028, h=0.018, loc=(hinge_x - 0.003, 0, LID_Z + 0.0025),
         rot=(math.pi / 2, 0, 0), material=ALU_CAST, seg=24, base=False)

# ---------------------------------------------------------------- 안전밸브(보일러 -Y 면)
zv = 0.046
av = apothem_at(BOILER, zv)
slope = math.atan2(apothem_at(BOILER, 0.038) - apothem_at(BOILER, 0.056), 0.018)
nut = cylinder('Safety_Valve_Nut', r=0.0068, h=0.0045, seg=6, material=STEEL, base=True)
cap = cylinder('Safety_Valve_Cap', r=0.0032, h=0.0030, seg=24, material=STEEL, base=True, loc=(0, 0, 0.0045))
nut.rotation_euler = (math.pi / 2 - slope, 0, 0)
nut.location = (0, -av + 0.0004, zv)
off = 0.0045
cap.location = (0, -av + 0.0004 - off * math.cos(slope), zv + off * math.sin(slope))
cap.rotation_euler = (math.pi / 2 - slope, 0, 0)

# ---------------------------------------------------------------- 커피통 옆면(-Y)의 타원 배지(상표 그림은 생략)
zb = 0.140
ab = apothem_at(COLLECTOR, zb)
tilt = math.atan2(apothem_at(COLLECTOR, 0.150) - apothem_at(COLLECTOR, 0.130), 0.020)
for nm, sx, sz, d, m in (('Badge_Rim', 0.0078, 0.0100, 0.0009, ALU_CAST), ('Badge_Face', 0.0062, 0.0082, 0.0014, BADGE)):
    b = cylinder(nm, r=1.0, h=d, seg=48, material=m, base=True)
    b.scale = (sx, sz, 1.0)
    b.rotation_euler = (math.pi / 2 + tilt, 0, 0)
    b.location = (0, -ab + 0.0003, zb)
