# 수동 타자기 (1920년대 프런트 스트라이크형, 언더우드 No.5 계열) — 실제 크기(폭 약 42cm) 모델.
# 검은 에나멜 몸체는 측면 프로필을 돌출한 뒤 불리언으로 활자대 바구니 자리를 파냈고,
# 반원형으로 펼쳐진 활자대(타이프바), 4단 계단식 둥근 키, 고무 플래튼과 캐리지, 리본 스풀,
# 종이 받침과 끼워진 종이를 bmesh 기본 도형(행렬 변환으로 한 메시에 누적)과 루프로 만들었다.
# 키 글자는 3D 텍스트로 얹었고, 질감 이미지 없이 Principled BSDF 상수 재질만 썼다.
from atelier import *

SHOT = {"azimuth": -58, "elevation": 26, "lens": 60, "light": "studio", "background": "#6d7a7e"}

# ---------------------------------------------------------------- 재질
ENAMEL = mat('Black_Enamel', '#1b1b1d', rough=0.22, coat=0.6)
CHROME = mat('Nickel_Chrome', '#c8cbcf', rough=0.14, metal=1.0)
STEEL = mat('Typebar_Steel', '#b4b7bb', rough=0.28, metal=1.0)
KEYTOP = mat('Key_Celluloid', '#ece3cc', rough=0.3)
LETTER = mat('Key_Lettering', '#26221e', rough=0.5)
RUBBER = mat('Platen_Rubber', '#2a2828', rough=0.65)
GOLD = mat('Gold_Decal', '#c9a24c', rough=0.3, metal=1.0)
PAPER = mat('Paper_Sheet', '#eee8d9', rough=0.85)
RIBBON = mat('Ink_Ribbon', '#6e1d1f', rough=0.7)
FELT = mat('Felt_Pad', '#4d3f33', rough=0.95)
GLASS = mat('Side_Glass', '#2c3a3d', rough=0.08)


# ---------------------------------------------------------------- bmesh 누적 도우미
def M(loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1)):
    return Matrix.LocRotScale(Vector(loc), Euler(rot), Vector(scale))


class Parts:
    """여러 기본 도형을 한 메시에 모은다(오브젝트 수를 줄이고 같은 재질끼리 묶기)."""

    def __init__(self, name, material):
        self.name, self.material, self.bm = name, material, bmesh.new()

    def box(self, loc, size, rot=(0, 0, 0)):
        bmesh.ops.create_cube(self.bm, size=1.0, matrix=M(loc, rot, size))

    def cyl(self, loc, r, h, rot=(0, 0, 0), seg=24, r2=None):
        bmesh.ops.create_cone(self.bm, cap_ends=True, segments=seg, radius1=r,
                              radius2=r if r2 is None else r2, depth=h, matrix=M(loc, rot))

    def rod(self, a, b, r, seg=8):
        a, b = Vector(a), Vector(b)
        d = b - a
        q = d.to_track_quat('Z', 'Y')
        mtx = Matrix.Translation((a + b) / 2) @ q.to_matrix().to_4x4()
        bmesh.ops.create_cone(self.bm, cap_ends=True, segments=seg, radius1=r, radius2=r,
                              depth=d.length, matrix=mtx)

    def strip(self, a, b, width, thick):
        """a→b 를 잇는 얇은 띠(폭 방향은 수직에 가깝게)."""
        a, b = Vector(a), Vector(b)
        d = b - a
        q = d.to_track_quat('X', 'Z')
        mtx = Matrix.Translation((a + b) / 2) @ q.to_matrix().to_4x4() @ Matrix.Diagonal((d.length, thick, width, 1))
        bmesh.ops.create_cube(self.bm, size=1.0, matrix=mtx)

    def done(self, smooth_it=False):
        me = bpy.data.meshes.new(self.name)
        self.bm.to_mesh(me)
        self.bm.free()
        ob = bpy.data.objects.new(self.name, me)
        link(ob)
        assign(ob, self.material)
        if smooth_it:
            smooth(ob)
        return ob


def yz_profile(name, pts, width, material, x=0.0):
    """YZ 평면 프로필 [(y,z),...] 을 X 방향으로 width 만큼 돌출."""
    bm = bmesh.new()
    h = width / 2
    a = [bm.verts.new((x - h, y, z)) for y, z in pts]
    b = [bm.verts.new((x + h, y, z)) for y, z in pts]
    bm.faces.new(a)
    bm.faces.new(list(reversed(b)))
    n = len(pts)
    for i in range(n):
        bm.faces.new((a[i], a[(i + 1) % n], b[(i + 1) % n], b[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)
    return assign(ob, material)


# ---------------------------------------------------------------- 주요 치수
BODY_W = 0.30          # 몸체 폭
BASE_TOP = 0.025       # 받침 윗면
DECK = 0.150           # 몸체 윗면
PX, PY, PZ = 0.0, 0.050, 0.205   # 플래튼 중심
PR = 0.020             # 플래튼 반지름
PLATEN_L = 0.33
BASKET_C = (0.0, 0.050)  # 활자대 바구니 중심(인자점 바로 아래)
BASKET_R = 0.118

# ---------------------------------------------------------------- 받침과 고무 발
base = box('Base_Frame', (BODY_W + 0.012, 0.355, BASE_TOP - 0.006), (0, -0.025, 0.006), material=ENAMEL, base=True)
bevel(base, 0.006, 3)
feet = Parts('Rubber_Feet', RUBBER)
for sx in (-1, 1):
    for fy in (-0.185, 0.135):
        feet.cyl((sx * 0.135, fy, 0.003), 0.012, 0.006, seg=20)
feet.done(True)

# ---------------------------------------------------------------- 몸체 (측면 프로필 돌출 + 바구니 파내기)
body_prof = [(-0.108, BASE_TOP - 0.002), (0.150, BASE_TOP - 0.002), (0.150, 0.138), (0.138, DECK),
             (-0.074, DECK), (-0.090, 0.136), (-0.108, 0.104)]
body = yz_profile('Body_Shell', body_prof, BODY_W, ENAMEL)
bevel(body, 0.004, 2)

# 활자대 바구니: 앞쪽 반원 + 뒤로 이어진 사각형 프리즘을 빼낸다
cut_pts = []
for i in range(25):
    a = math.pi + math.pi * i / 24          # 180°→360° (앞쪽 반원)
    cut_pts.append((BASKET_C[0] + BASKET_R * math.cos(a), BASKET_C[1] + BASKET_R * math.sin(a)))
cut_pts += [(BASKET_R, 0.085), (-BASKET_R, 0.085)]
cutter = prism('Basket_Cutter', cut_pts, 0.12, loc=(0, 0, 0.100))
boolean(body, cutter)

# 바구니 바닥(어두운 펠트)
floor_pts = [(BASKET_C[0] + (BASKET_R + 0.001) * math.cos(math.pi + math.pi * i / 24),
              BASKET_C[1] + (BASKET_R + 0.001) * math.sin(math.pi + math.pi * i / 24)) for i in range(25)]
floor_pts += [(BASKET_R + 0.001, 0.086), (-BASKET_R - 0.001, 0.086)]
prism('Basket_Floor', floor_pts, 0.004, loc=(0, 0, 0.098), material=FELT)

# ---------------------------------------------------------------- 활자대(타이프바) 부채꼴
seg_parts = Parts('Typebar_Segment', CHROME)
seg_r0, seg_r1 = 0.042, 0.050
# 반원형 세그먼트(활자대 축받이)
for i in range(24):
    a0 = math.pi + math.pi * i / 24
    a1 = math.pi + math.pi * (i + 1) / 24
    am = (a0 + a1) / 2
    rm = (seg_r0 + seg_r1) / 2
    seg_parts.box((BASKET_C[0] + rm * math.cos(am), BASKET_C[1] + rm * math.sin(am), 0.134),
                  (seg_r1 - seg_r0, rm * (a1 - a0) * 1.04, 0.022), (0, 0, am))
seg_parts.done()

bars = Parts('Typebars', STEEL)
slugs = Parts('Type_Slugs', STEEL)
N_BARS = 42
for i in range(N_BARS):
    a = math.radians(192 + 156 * i / (N_BARS - 1))
    ca, sa = math.cos(a), math.sin(a)
    r_in, r_out = 0.047, 0.110
    z_in, z_out = 0.138, 0.109
    p0 = (BASKET_C[0] + r_in * ca, BASKET_C[1] + r_in * sa, z_in)
    p1 = (BASKET_C[0] + r_out * ca, BASKET_C[1] + r_out * sa, z_out)
    L = math.dist(p0, p1)
    slope = math.atan2(z_in - z_out, r_out - r_in)
    mid = [(u + v) / 2 for u, v in zip(p0, p1)]
    bars.box(mid, (L, 0.0030, 0.0040), (0, slope, a))
    slugs.box((p1[0] - 0.002 * ca, p1[1] - 0.002 * sa, z_out + 0.0035), (0.007, 0.0034, 0.0045), (0, slope, a))
bars.done()
slugs.done()
# 활자대 받침(펠트 반원 링)
rest = Parts('Typebar_Rest', FELT)
for i in range(20):
    a = math.radians(190 + 160 * (i + 0.5) / 20)
    rest.box((BASKET_C[0] + 0.104 * math.cos(a), BASKET_C[1] + 0.104 * math.sin(a), 0.1045),
             (0.006, 0.104 * math.radians(160 / 20) * 1.05, 0.005), (0, 0, a))
rest.done()

# ---------------------------------------------------------------- 키보드 (4단 계단식)
ROWS = [  # (y, 키 윗면 z, x 오프셋, 글자)
    (-0.182, 0.058, 0.012, "ZXCVBNM,./"),
    (-0.160, 0.071, 0.007, "ASDFGHJKL;'"),
    (-0.138, 0.084, 0.002, "QWERTYUIOP-"),
    (-0.116, 0.097, -0.003, "23456789_0½"),
]
PITCH = 0.0195
KEY_R = 0.0078
rims = Parts('Key_Rims', CHROME)
tops = Parts('Key_Tops', KEYTOP)
stems = Parts('Key_Levers', ENAMEL)
LEVER_Z = 0.036
key_positions = []
for (ky, kz, xo, chars) in ROWS:
    n = len(chars)
    for j, ch in enumerate(chars):
        kx = xo + (j - (n - 1) / 2) * PITCH
        key_positions.append((kx, ky, kz, ch))
# 시프트·백스페이스 등 가장자리 키
extra = [(-0.122, -0.182, 0.058, ''), (0.135, -0.182, 0.058, ''), (-0.124, -0.160, 0.071, ''),
         (0.118, -0.116, 0.097, '')]
key_positions += extra
for kx, ky, kz, ch in key_positions:
    rims.cyl((kx, ky, kz - 0.0025), KEY_R, 0.005, seg=24)
    tops.cyl((kx, ky, kz + 0.0003), KEY_R - 0.0012, 0.0006, seg=24)
    # 키 줄기(아래로) + 키 레버(뒤쪽 몸체로)
    stems.box((kx, ky + 0.003, (kz - 0.005 + LEVER_Z) / 2), (0.0016, 0.003, kz - 0.005 - LEVER_Z))
    stems.box((kx, (ky - 0.108) / 2 + 0.002, LEVER_Z), (0.0022, -0.108 - ky + 0.008, 0.005))
rims.done(True)
tops.done(True)
stems.done()

# 키 글자 (3D 텍스트)
letters = []
for kx, ky, kz, ch in key_positions:
    if not ch:
        continue
    t = text3d('Letter_' + ch, ch, size=0.0085, depth=0.00025, loc=(kx, ky - 0.0028, kz + 0.0009), material=LETTER)
    letters.append(t)

# 스페이스바
space = Parts('Space_Bar', ENAMEL)
space.box((0.0, -0.203, 0.049), (0.21, 0.009, 0.007))
for sx in (-0.085, 0.085):
    space.box((sx, -0.203, (0.046 + LEVER_Z) / 2), (0.003, 0.003, 0.046 - LEVER_Z))
    space.box((sx, (-0.203 - 0.108) / 2, LEVER_Z - 0.004), (0.003, 0.095, 0.004))
space.done()
bevel(bpy.data.objects['Space_Bar'], 0.002, 2)

# 키보드 아래 받침 앞판(레버 가림판)
box('Key_Comb', (BODY_W - 0.01, 0.006, 0.010), (0, -0.112, LEVER_Z - 0.003), material=ENAMEL, base=True)

# ---------------------------------------------------------------- 캐리지
car = Parts('Carriage_Frame', ENAMEL)
car.box((0, 0.112, 0.156), (0.40, 0.065, 0.012))                     # 캐리지 받침대
for sx in (-1, 1):
    # 플래튼 축을 잡는 끝판
    car.box((sx * 0.172, 0.085, 0.188), (0.008, 0.10, 0.052))
car.done()
bevel(bpy.data.objects['Carriage_Frame'], 0.002, 2)

rails = Parts('Carriage_Rails', CHROME)
rails.rod((-0.20, 0.140, 0.165), (0.20, 0.140, 0.165), 0.004, seg=12)
rails.rod((-0.20, 0.084, 0.165), (0.20, 0.084, 0.165), 0.004, seg=12)
rails.box((0, 0.020, 0.176), (0.36, 0.004, 0.012))                    # 앞쪽 눈금자
rails.done(True)

platen = cylinder('Platen', PR, PLATEN_L, (PX - PLATEN_L / 2, PY, PZ), (0, math.pi / 2, 0), RUBBER, seg=48, base=True)
smooth(platen)
knob = Parts('Platen_Knobs', ENAMEL)
for sx in (-1, 1):
    knob.cyl((sx * 0.1765, PY, PZ), 0.004, 0.006, (0, math.pi / 2, 0), seg=16)
    knob.cyl((sx * 0.193, PY, PZ), 0.022, 0.026, (0, math.pi / 2, 0), seg=40)
knob.done(True)
bevel(bpy.data.objects['Platen_Knobs'], 0.003, 2)

# 종이 받침(플래튼 뒤 곡면판)
tbl = []
for i in range(13):
    t = i / 12
    tbl.append((PY + (PR + 0.006) * math.cos(math.radians(60)) + 0.085 * t, PZ + 0.012 + 0.085 * t - 0.015 * t * t))
inner = [(y + 0.0, z) for y, z in tbl]
outer = [(y + 0.0025 * 0.7, z - 0.0025 * 0.7) for y, z in tbl]
paper_table = yz_profile('Paper_Table', inner + list(reversed(outer)), 0.27, ENAMEL)

# 끼워진 종이: 인자점 앞에서 플래튼을 감고 종이 받침을 따라 올라간다
sheet_pts = []
for i in range(14):
    a = math.radians(200 - 140 * i / 13)          # 플래튼 앞-아래(200°)에서 위-뒤(60°)로
    sheet_pts.append((PY + (PR + 0.0012) * math.cos(a), PZ + (PR + 0.0012) * math.sin(a)))
for y, z in tbl[1:]:
    sheet_pts.append((y - 0.004, z + 0.004))
y_last, z_last = tbl[-1]
for k in range(1, 4):
    sheet_pts.append((y_last - 0.004 + 0.012 * k, z_last + 0.004 + 0.022 * k))
sheet_in = sheet_pts
sheet_out = [(y, z + 0.0006) for y, z in sheet_pts]
paper = yz_profile('Paper_Sheet', sheet_in + list(reversed(sheet_out)), 0.216, PAPER)

# 용지 누름대(페이퍼 베일)와 롤러
bail = Parts('Paper_Bail', CHROME)
bz, by = PZ + PR * 0.75, PY - PR * 0.55
bail.rod((-0.16, by - 0.002, bz + 0.003), (0.16, by - 0.002, bz + 0.003), 0.0018, seg=10)
for sx in (-1, 1):
    bail.rod((sx * 0.16, by - 0.002, bz + 0.003), (sx * 0.168, PY + 0.01, PZ + 0.004), 0.0018, seg=8)
for rx in (-0.06, 0.06):
    bail.cyl((rx, by - 0.001, bz + 0.0015), 0.004, 0.012, (0, math.pi / 2, 0), seg=16)
bail.done(True)

# 캐리지 리턴 레버(왼쪽)
lever = Parts('Carriage_Return_Lever', CHROME)
lever.rod((-0.176, 0.040, 0.215), (-0.200, 0.010, 0.222), 0.003)
lever.rod((-0.200, 0.010, 0.222), (-0.225, -0.040, 0.232), 0.003)
lever.box((-0.230, -0.052, 0.234), (0.016, 0.026, 0.004), (0.15, 0, -0.35))
lever.done(True)

# ---------------------------------------------------------------- 리본 스풀과 리본
spools = Parts('Ribbon_Spool_Cups', CHROME)
ribs = Parts('Ribbon_Rolls', RIBBON)
for sx in (-1, 1):
    spools.cyl((sx * 0.124, -0.035, DECK + 0.002), 0.024, 0.004, seg=40)
    ribs.cyl((sx * 0.124, -0.035, DECK + 0.008), 0.019, 0.008, seg=40)
    spools.cyl((sx * 0.124, -0.035, DECK + 0.014), 0.004, 0.006, seg=16)
spools.done(True)
ribs.done(True)

# 리본 진동판(가운데 리본 가이드)
guide = Parts('Ribbon_Vibrator', CHROME)
guide.box((0, PY - PR - 0.004, PZ - 0.012), (0.016, 0.002, 0.012))
guide.done()

# ---------------------------------------------------------------- 금색 줄무늬
gold = Parts('Gold_Pinstripes', GOLD)
gold.box((0, 0.1505, 0.110), (0.26, 0.0012, 0.0025))
gold.done()

# 리본 띠: 양쪽 스풀에서 가운데 리본 가이드로
band = Parts('Ribbon_Band', RIBBON)
for sx in (-1, 1):
    band.strip((sx * 0.106, -0.024, DECK + 0.009), (sx * 0.035, 0.012, PZ - 0.020), 0.010, 0.0008)
    band.strip((sx * 0.035, 0.012, PZ - 0.020), (sx * 0.008, PY - PR - 0.0055, PZ - 0.012), 0.010, 0.0008)
band.done()

# 측면 유리창(양옆)과 금색 테두리
for sx in (-1, 1):
    xs = sx * (BODY_W / 2 + 0.0007)
    win = [(-0.040, 0.050), (0.120, 0.050), (0.120, 0.125), (-0.040, 0.125)]
    yz_profile('Side_Window_' + ('L' if sx < 0 else 'R'), win, 0.0014, GLASS, x=xs)
    fr = Parts('Side_Window_Trim_' + ('L' if sx < 0 else 'R'), GOLD)
    xf = sx * (BODY_W / 2 + 0.0016)
    for (y0, z0), (y1, z1) in zip(win, win[1:] + win[:1]):
        fr.rod((xf, y0, z0), (xf, y1, z1), 0.0012, seg=6)
    fr.done()

# 앞면 경사판의 금색 장식선과 가운데 상표판 자리
front_gold = Parts('Front_Gold_Decal', GOLD)
slope = math.atan2(0.136 - 0.104, -0.090 + 0.108)          # 경사판 기울기
nrm = Vector((0, -math.sin(slope), math.cos(slope)))
for t in (0.18, 0.82):
    c = Vector((0, -0.108 + 0.018 * t, 0.104 + 0.032 * t)) + nrm * 0.0007
    front_gold.box(c, (0.25, 0.0012, 0.0015), (math.pi / 2 + slope, 0, 0))
cen = Vector((0, -0.099, 0.120)) + nrm * 0.0008
front_gold.cyl(cen, 0.008, 0.0012, (slope, 0, 0), seg=32)
front_gold.done()

# 뒷면 메인스프링 드럼(캐리지를 당기는 태엽통)
drum = Parts('Mainspring_Drum', ENAMEL)
drum.cyl((-0.100, 0.1535, 0.090), 0.030, 0.008, (math.pi / 2, 0, 0), seg=40)
drum.cyl((-0.100, 0.1590, 0.090), 0.006, 0.004, (math.pi / 2, 0, 0), seg=16)
drum.done(True)

# 여백 조절 막대(캐리지 뒤 위)와 여백 멈추개, 용지 해제 레버, 줄 간격 래칫
mr = Parts('Margin_Rack', CHROME)
mr.box((0, 0.136, 0.176), (0.36, 0.006, 0.010))
for mx in (-0.11, 0.12):
    mr.box((mx, 0.136, 0.184), (0.010, 0.010, 0.008))
mr.rod((0.172, 0.105, 0.208), (0.170, 0.128, 0.240), 0.0022)               # 용지 해제 레버(오른쪽)
mr.box((0.170, 0.130, 0.243), (0.006, 0.012, 0.006))
mr.cyl((-0.1775, PY, PZ), 0.014, 0.003, (0, math.pi / 2, 0), seg=30)        # 줄 간격 래칫 휠
mr.done(True)
