# 현대 쏘나타 2세대 출시형(Y2, 1988) — 각진 3박스 세단을 bpy 코드로만 만든 모델.
# 차체 하부는 길이 방향으로 단면(모서리를 둥글린 다각형)을 이어 붙인 로프트 메시이고,
# 바퀴 구멍은 원통 불리언으로 파냈다. 캐빈은 사다리꼴 절두체에 베벨을 준 뒤
# 앞·뒤·옆 유리를 각 평면 위에 얇은 판으로 올렸다. 사각 헤드램프, 크롬 범퍼,
# 가로로 긴 테일램프, 측면 몰딩과 휠캡은 박스·회전체·관(curve) 조합이다.
# 바퀴 네 개는 메시를 공유하는 링크 복제본이다. 단위는 미터, 앞이 -Y 방향.
from atelier import *

SHOT = {"azimuth": -50, "elevation": 13, "lens": 55, "light": "studio", "background": "#3b4552"}

# ── 재질 ───────────────────────────────────────────────
PAINT = mat("Paint_WineRed", "#6d1a26", rough=0.28, metal=0.35, coat=1.0)
CHROME = mat("Chrome", "#d6dadf", rough=0.12, metal=1.0)
TRIM = mat("Trim_BlackSatin", "#1d1e21", rough=0.55)
GLASS = mat("Glass_Tinted", "#1c2a33", rough=0.04, metal=0.2, coat=1.0)
LENS = mat("Headlamp_Lens", "#c9d4da", rough=0.06, metal=0.55)
RED = mat("Taillamp_Red", "#9c1016", rough=0.18)
AMBER = mat("Lamp_Amber", "#d27a12", rough=0.2)
TIRE = mat("Tire_Rubber", "#262628", rough=0.88)
WHEEL = mat("Hubcap_Silver", "#b8bcc2", rough=0.3, metal=0.85)
DARK = mat("WheelWell_Dark", "#151618", rough=0.9)
PLATE = mat("Plate_White", "#e3e1d8", rough=0.5)

# ── 주요 치수 (L 4.68 · W 1.75 · H 1.40 · 휠베이스 2.65) ───
Y_FRONT, Y_REAR = -2.34, 2.34
AXLE_F, AXLE_R = -1.39, 1.26
WHEEL_R = 0.31
HW = 0.875          # 차체 반폭
BELT = 0.905        # 벨트라인 높이
ROOF = 1.40


def set_sharp(ob, deg=35):
    for p in ob.data.polygons:
        p.use_smooth = True
    ob.data.set_sharp_from_angle(angle=math.radians(deg))
    return ob


# ── 1. 하부 차체: 단면 로프트 ─────────────────────────────
# (y, 윗면 높이 zt, 반폭 hw, 바닥 zb)
STATIONS = [
    (-2.340, 0.760, 0.800, 0.250),
    (-2.334, 0.778, 0.830, 0.240),
    (-2.315, 0.790, 0.852, 0.232),
    (-2.280, 0.800, 0.866, 0.226),
    (-2.200, 0.812, 0.874, 0.222),
    (-2.000, 0.832, 0.875, 0.220),
    (-1.600, 0.862, 0.875, 0.215),
    (-1.200, 0.890, 0.875, 0.210),
    (-0.800, 0.915, 0.875, 0.205),
    (-0.550, 0.930, 0.875, 0.205),
    (0.000, 0.935, 0.875, 0.205),
    (0.800, 0.945, 0.875, 0.205),
    (1.400, 0.958, 0.875, 0.210),
    (1.800, 0.968, 0.875, 0.220),
    (2.150, 0.972, 0.874, 0.235),
    (2.260, 0.970, 0.866, 0.245),
    (2.315, 0.962, 0.852, 0.252),
    (2.334, 0.950, 0.830, 0.258),
    (2.340, 0.935, 0.800, 0.262),
]
# 벨트라인을 낮춰 80년대 세단 특유의 넓은 유리 면적을 만든다(앞코는 그대로, 뒤로 갈수록 2.5cm 낮춤)
STATIONS = [(y, zt - 0.025 * min(1.0, max(0.0, (y + 2.34) / 1.6)), hw, zb) for y, zt, hw, zb in STATIONS]


def refine(st, sub=3):
    """단면 사이를 선형 보간해 로프트를 촘촘하게."""
    out = []
    for a, b in zip(st, st[1:]):
        for i in range(sub):
            t = i / sub
            out.append(tuple(a[k] + (b[k] - a[k]) * t for k in range(4)))
    out.append(st[-1])
    return out


def section(zt, hw, zb):
    """오른쪽 반 단면 (x, z) — 아래 중앙에서 위 중앙까지."""
    h = zt - zb
    return [
        (0.0, zb),
        (hw - 0.10, zb),
        (hw - 0.035, zb + 0.03),
        (hw - 0.010, zb + 0.12),
        (hw, zb + 0.30 * h),
        (hw, zb + 0.62 * h),          # 캐릭터 라인
        (hw - 0.006, zb + 0.66 * h),
        (hw - 0.010, zt - 0.07),
        (hw - 0.030, zt - 0.018),
        (hw - 0.075, zt),
        (hw * 0.5, zt + 0.010),
        (0.0, zt + 0.014),
    ]


def build_lower_body():
    bm = bmesh.new()
    rings = []
    for y, zt, hw, zb in refine(STATIONS):
        half = section(zt, hw, zb)
        full = half + [(-x, z) for x, z in reversed(half[1:-1])]
        rings.append([bm.verts.new((x, y, z)) for x, z in full])
    n = len(rings[0])
    for a, b in zip(rings, rings[1:]):
        for i in range(n):
            bm.faces.new((a[i], a[(i + 1) % n], b[(i + 1) % n], b[i]))
    bm.faces.new(rings[0])
    bm.faces.new(list(reversed(rings[-1])))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new("Body_Lower")
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new("Body_Lower", me)
    link(ob)
    assign(ob, PAINT)
    ob.data.materials.append(DARK)
    # 휠하우스: 좌우 각각 얕은 원통으로 파낸다
    for ax in (AXLE_F, AXLE_R):
        for s in (1, -1):
            c = cylinder(f"Cutter_Arch_{'F' if ax < 0 else 'R'}{'R' if s > 0 else 'L'}",
                         r=0.375, h=0.36, loc=(s * (HW - 0.30 + 0.18), ax, WHEEL_R + 0.01),
                         rot=(0, math.pi / 2, 0), base=False, seg=48, material=DARK)
            m = ob.modifiers.new("Arch", 'BOOLEAN')
            m.object = c
            m.solver = 'EXACT'
            m.material_mode = 'TRANSFER'
            c.hide_render = True
            c.hide_viewport = True
            c['atelier_cutter'] = True
    apply_all(ob)
    set_sharp(ob, 40)
    return ob


body = build_lower_body()


def body_top(y):
    """해당 y 에서 차체 윗면 높이(중앙)."""
    for (y0, z0, *_), (y1, z1, *_) in zip(STATIONS, STATIONS[1:]):
        if y0 <= y <= y1:
            t = (y - y0) / (y1 - y0)
            return z0 + (z1 - z0) * t
    return STATIONS[-1][1]


# ── 2. 캐빈(그린하우스): 사다리꼴 절두체 ──────────────────
CAB_Y0, CAB_Y1 = -0.66, 1.56      # 벨트라인에서 앞유리/뒷유리 밑단
ROOF_Y0, ROOF_Y1 = 0.06, 1.04     # 지붕 앞/뒤 끝
CAB_HW, ROOF_HW = 0.835, 0.670
CAB_Z0 = 0.90                     # 차체 속에 묻히는 밑면


def side_x(z):
    """캐빈 옆면 평면 위 x (높이 z)."""
    return CAB_HW - (z - BELT) * (CAB_HW - ROOF_HW) / (ROOF - BELT)


def build_cabin():
    bm = bmesh.new()
    zb = CAB_Z0
    # 밑면을 BELT 아래로 연장해도 옆면이 같은 평면이 되도록 x 를 맞춘다
    xb = side_x(zb)
    yb0 = CAB_Y0 - (BELT - zb) * (ROOF_Y0 - CAB_Y0) / (ROOF - BELT)
    yb1 = CAB_Y1 + (BELT - zb) * (CAB_Y1 - ROOF_Y1) / (ROOF - BELT)
    B = [bm.verts.new(v) for v in ((-xb, yb0, zb), (xb, yb0, zb), (xb, yb1, zb), (-xb, yb1, zb))]
    T = [bm.verts.new(v) for v in ((-ROOF_HW, ROOF_Y0, ROOF), (ROOF_HW, ROOF_Y0, ROOF),
                                    (ROOF_HW, ROOF_Y1, ROOF), (-ROOF_HW, ROOF_Y1, ROOF))]
    bm.faces.new(list(reversed(B)))
    bm.faces.new((B[0], B[1], T[1], T[0]))           # 앞유리 면
    bm.faces.new((B[1], B[2], T[2], T[1]))           # 오른쪽
    bm.faces.new((B[2], B[3], T[3], T[2]))           # 뒷유리 면
    bm.faces.new((B[3], B[0], T[0], T[3]))           # 왼쪽
    bm.faces.new(T)                                  # 지붕
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new("Cabin")
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new("Cabin_Greenhouse", me)
    link(ob)
    assign(ob, PAINT)
    bevel(ob, width=0.035, segments=4, angle=20)
    apply_all(ob)
    set_sharp(ob, 50)
    return ob


cabin = build_cabin()

# ── 3. 바퀴 ─────────────────────────────────────────────
def build_wheel():
    tw = 0.093  # 타이어 반폭
    prof = [(0.180, -tw + 0.012), (0.205, -tw), (0.265, -tw - 0.004), (0.295, -tw + 0.012),
            (WHEEL_R, -tw + 0.04), (WHEEL_R, tw - 0.04), (0.295, tw - 0.012), (0.265, tw + 0.004),
            (0.205, tw), (0.180, tw - 0.012)]
    tire = lathe("Tire", prof, material=TIRE, seg=96)
    # 휠캡: 바깥(+z) 쪽으로 볼록한 원판
    cap_prof = [(0.0, 0.040), (0.05, 0.039), (0.10, 0.034), (0.145, 0.026), (0.170, 0.010),
                (0.182, 0.0), (0.182, -0.03), (0.0, -0.03)]
    cap = lathe("Hubcap", cap_prof, material=WHEEL, seg=96)
    parts = [tire, cap]
    # 휠캡 구멍(통풍 슬롯) 8개
    for i in range(8):
        a = 2 * math.pi * i / 8
        s = box(f"Hubcap_Slot_{i}", (0.045, 0.018, 0.012),
                loc=(0.115 * math.cos(a), 0.115 * math.sin(a), 0.031), rot=(0, 0, a), material=DARK)
        parts.append(s)
    hub = cylinder("Hub_Center", r=0.038, h=0.012, loc=(0, 0, 0.036), material=CHROME, seg=32)
    parts.append(hub)
    w = join(parts, "Wheel_FR")
    for p in w.data.polygons:
        p.use_smooth = True
    w.data.set_sharp_from_angle(angle=math.radians(40))
    return w


wheel = build_wheel()
TRACK_X = 0.735
wheel.rotation_euler = (0, math.pi / 2, 0)
wheel.location = (TRACK_X, AXLE_F, WHEEL_R)
duplicate(wheel, loc=(TRACK_X, AXLE_R, WHEEL_R), name="Wheel_RR")
duplicate(wheel, loc=(-TRACK_X, AXLE_F, WHEEL_R), rot=(0, -math.pi / 2, 0), name="Wheel_FL")
duplicate(wheel, loc=(-TRACK_X, AXLE_R, WHEEL_R), rot=(0, -math.pi / 2, 0), name="Wheel_RL")


# ── 4. 유리: 각 캐빈 면 위에 얇은 판 ─────────────────────
def slab(name, pts, normal, material, off=0.004, th=0.006):
    """평면 위 다각형 pts(3D)를 normal 방향으로 off 띄우고 th 두께의 판으로."""
    n = Vector(normal).normalized()
    bm = bmesh.new()
    lo = [bm.verts.new(Vector(p) + n * off) for p in pts]
    hi = [bm.verts.new(Vector(p) + n * (off + th)) for p in pts]
    bm.faces.new(list(reversed(lo)))
    bm.faces.new(hi)
    k = len(pts)
    for i in range(k):
        bm.faces.new((lo[i], lo[(i + 1) % k], hi[(i + 1) % k], hi[i]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return _slab_obj(name, bm, material)


def _slab_obj(name, bm, material):
    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)


def side_pt(s, y, z):
    return (s * side_x(z), y, z)


SIDE_N = (ROOF - BELT, 0, CAB_HW - ROOF_HW)  # 오른쪽 옆면 바깥 법선(x,z 성분)


def a_line(z):   # A 필러 앞 모서리 y
    return CAB_Y0 + (z - BELT) * (ROOF_Y0 - CAB_Y0) / (ROOF - BELT)


def c_line(z):   # C 필러 뒤 모서리 y
    return CAB_Y1 - (z - BELT) * (CAB_Y1 - ROOF_Y1) / (ROOF - BELT)


WZ0, WZ1 = 0.955, 1.355   # 옆창 밑/윗단
for s, tag in ((1, "R"), (-1, "L")):
    nrm = (s * SIDE_N[0], 0, SIDE_N[2])
    # 검은 창틀 띠(유리 뒤)
    frame = [(a_line(WZ0 - 0.012) + 0.075, WZ0 - 0.012), (c_line(WZ0 - 0.012) - 0.11, WZ0 - 0.012),
             (c_line(WZ1 + 0.012) - 0.11, WZ1 + 0.012), (a_line(WZ1 + 0.012) + 0.075, WZ1 + 0.012)]
    slab(f"Window_Frame_{tag}", [side_pt(s, y, z) for y, z in frame][::s], nrm, TRIM, off=0.001, th=0.003)
    wins = {
        "Front": [(a_line(WZ0) + 0.095, WZ0), (0.40, WZ0), (0.40, WZ1), (a_line(WZ1) + 0.095, WZ1)],
        "Rear": [(0.47, WZ0), (1.00, WZ0), (1.00, WZ1), (0.47, WZ1)],
        "Quarter": [(1.05, WZ0), (c_line(WZ0) - 0.13, WZ0), (c_line(WZ1) - 0.13, WZ1), (1.05, WZ1)],
    }
    for k, poly in wins.items():
        pts = [side_pt(s, y, z) for y, z in poly]
        if poly[2][0] <= poly[3][0] + 1e-3:
            pts = pts[:2] + [pts[3]]
        slab(f"Glass_Side_{k}_{tag}", pts[::s], nrm, GLASS, off=0.003, th=0.004)

# 앞유리 / 뒷유리
def screen(name, yb, yt, sign):
    zb_, zt_ = BELT + 0.03, ROOF - 0.035
    def y_at(z):
        return yb + (z - BELT) * (yt - yb) / (ROOF - BELT)
    def hw_at(z):
        return side_x(z) - 0.075
    pts = [(-hw_at(zb_), y_at(zb_), zb_), (hw_at(zb_), y_at(zb_), zb_),
           (hw_at(zt_), y_at(zt_), zt_), (-hw_at(zt_), y_at(zt_), zt_)]
    dy, dz = (yt - yb), (ROOF - BELT)
    nrm = (0, -dz * sign, dy * sign)
    if sign < 0:
        nrm = (0, dz, -dy)
    slab(name, pts, nrm, GLASS, off=0.003, th=0.004)


screen("Glass_Windshield", CAB_Y0, ROOF_Y0, 1)
screen("Glass_Rear", CAB_Y1, ROOF_Y1, -1)


# ── 5. 차체 표면을 따라가는 보조 함수 ─────────────────────
def station_at(y):
    y = max(STATIONS[0][0], min(STATIONS[-1][0], y))
    for a, b in zip(STATIONS, STATIONS[1:]):
        if a[0] <= y <= b[0]:
            t = (y - a[0]) / (b[0] - a[0])
            return [a[i] + (b[i] - a[i]) * t for i in (1, 2, 3)]


def surf_x(y, z):
    """오른쪽 옆면에서 (y, z) 의 x."""
    zt, hw, zb = station_at(y)
    pts = section(zt, hw, zb)[1:10]
    for (x0, z0), (x1, z1) in zip(pts, pts[1:]):
        if z0 <= z <= z1:
            return x0 + (x1 - x0) * (z - z0) / (z1 - z0)
    return hw


def surf_z(x, y):
    """윗면에서 (x, y) 의 z."""
    zt, hw, zb = station_at(y)
    ax = abs(x)
    if ax <= hw * 0.5:
        return zt + 0.014 - 0.004 * ax / (hw * 0.5)
    return zt + 0.010 * (hw - 0.075 - ax) / (hw * 0.5 - 0.075)


GROOVE = 0.0045


def groove(name, pts):
    return curve_tube(name, pts, radius=GROOVE, material=DARK, resolution=2, bevel_res=1)


def side_line(name, yz, s=1):
    return groove(name, [(s * surf_x(y, z), y, z) for y, z in yz])


def top_line(name, xy):
    return groove(name, [(x, y, surf_z(x, y)) for x, y in xy])


def steps(a, b, n):
    return [a + (b - a) * i / n for i in range(n + 1)]


# 후드 / 트렁크 / 도어 분할선
top_line("Line_Hood_Rear", [(x, -0.70) for x in steps(-0.80, 0.80, 16)])
top_line("Line_Trunk_Front", [(x, 1.62) for x in steps(-0.80, 0.80, 16)])
for s, tag in ((1, "R"), (-1, "L")):
    top_line(f"Line_Hood_Side_{tag}", [(s * 0.80, y) for y in steps(-2.33, -0.70, 20)])
    top_line(f"Line_Trunk_Side_{tag}", [(s * 0.80, y) for y in steps(1.62, 2.33, 12)])
    side_line(f"Line_Door_Front_{tag}", [(-0.62, z) for z in steps(0.30, 0.90, 10)], s)
    side_line(f"Line_Door_B_{tag}", [(0.44, z) for z in steps(0.27, 0.90, 10)], s)
    arc = [(1.30, 0.90), (1.30, 0.74)]
    for a in steps(80, 165, 8):
        r = math.radians(a)
        arc.append((AXLE_R + 0.43 * math.cos(r), WHEEL_R + 0.02 + 0.43 * math.sin(r)))
    arc.append((0.84, 0.28))
    side_line(f"Line_Door_Rear_{tag}", arc, s)

# ── 6. 범퍼(크롬 + 고무 띠), 앞 스커트 ─────────────────────
def bumper_outline(grow=0.0):
    outer = [(0.89, -2.05), (0.89, -2.27), (0.85, -2.385), (0.72, -2.425)]
    inner = [(0.70, -2.33), (0.80, -2.322), (0.845, -2.29), (0.862, -2.25), (0.862, -2.05)]
    out = []
    for x, y in outer:
        out.append((x + grow * (1 if x > 0.8 else 0.3), y - grow * (1 if y < -2.3 else 0)))
    right = out + inner[::-1][:0]
    pts = right + [(-x, y) for x, y in reversed(right)] + [(-x, y) for x, y in inner] + list(reversed(inner))
    return pts


def build_bumper(name):
    parts = []
    bar = prism(name + "_Chrome", bumper_outline(), 0.17, loc=(0, 0, 0.285), material=CHROME)
    bevel(bar, width=0.018, segments=3, angle=25)
    apply_all(bar)
    parts.append(bar)
    strip = prism(name + "_Rubber", bumper_outline(0.008), 0.035, loc=(0, 0, 0.36), material=TRIM)
    parts.append(strip)
    b = join(parts, name)
    set_sharp(b, 40)
    return b


front_bumper = build_bumper("Bumper_Front")
rear_bumper = duplicate(front_bumper, rot=(0, 0, math.pi), name="Bumper_Rear")
valance = box("Front_Valance", (1.56, 0.08, 0.09), loc=(0, -2.30, 0.235), material=TRIM)
bevel(valance, 0.01, 2)

# ── 7. 앞모습: 사각 헤드램프, 그릴, 엠블럼 ───────────────
YF = Y_FRONT
for s, tag in ((1, "R"), (-1, "L")):
    hx = s * 0.515
    bez = box(f"Headlamp_Bezel_{tag}", (0.39, 0.03, 0.155), loc=(hx, YF - 0.008, 0.633), material=CHROME)
    bevel(bez, 0.006, 2)
    box(f"Headlamp_Lens_{tag}", (0.37, 0.012, 0.135), loc=(hx, YF - 0.019, 0.633), material=LENS)
    # 렌즈 안 세로 칸살
    for i, dx in enumerate((-0.09, 0.0, 0.09)):
        box(f"Headlamp_Rib_{tag}{i}", (0.006, 0.006, 0.13), loc=(hx + dx, YF - 0.026, 0.633), material=CHROME)
    box(f"Corner_Lamp_{tag}", (0.085, 0.03, 0.13), loc=(s * 0.765, YF - 0.0, 0.633), material=AMBER)
    side_lamp = box(f"Side_Marker_{tag}", (0.02, 0.15, 0.10),
                    loc=(s * (surf_x(-2.17, 0.63) - 0.004), -2.17, 0.633), material=AMBER)
    bevel(side_lamp, 0.006, 2)

grille_bg = box("Grille_Back", (0.60, 0.03, 0.14), loc=(0, YF - 0.006, 0.633), material=DARK)
for i, (sz, lc) in enumerate((((0.62, 0.02, 0.012), (0, YF - 0.016, 0.708)),
                              ((0.62, 0.02, 0.012), (0, YF - 0.016, 0.558)),
                              ((0.012, 0.02, 0.16), (0.305, YF - 0.016, 0.633)),
                              ((0.012, 0.02, 0.16), (-0.305, YF - 0.016, 0.633)))):
    box(f"Grille_Frame_{i}", sz, loc=lc, material=CHROME)
for i, z in enumerate(steps(0.585, 0.681, 4)):
    box(f"Grille_Bar_{i}", (0.60, 0.012, 0.007), loc=(0, YF - 0.018, z), material=CHROME)
emb = torus("Emblem_Hyundai_Oval", R=0.052, r=0.005, loc=(0, YF - 0.028, 0.633),
            rot=(math.pi / 2, 0, 0), material=CHROME, seg=40, rseg=8)
emb.scale = (1.0, 0.62, 1.0)
box("Emblem_Back", (0.10, 0.008, 0.06), loc=(0, YF - 0.024, 0.633), material=CHROME).scale = (1, 1, 1)
for i, dx in enumerate((-0.018, 0.018)):
    box(f"Emblem_H_{i}", (0.008, 0.006, 0.05), loc=(dx, YF - 0.030, 0.633),
        rot=(0, math.radians(-20), 0), material=DARK)
box("Emblem_H_bar", (0.036, 0.006, 0.007), loc=(0, YF - 0.030, 0.633), material=DARK)

# ── 8. 뒷모습: 가로 테일램프, 가니시, 번호판, 배기구 ───────
YR = Y_REAR
for s, tag in ((1, "R"), (-1, "L")):
    rim = box(f"Taillamp_Housing_{tag}", (0.42, 0.024, 0.165), loc=(s * 0.60, YR + 0.006, 0.78), material=CHROME)
    bevel(rim, 0.005, 2)
    for k, (x0, x1, m) in enumerate(((0.63, 0.80, RED), (0.52, 0.63, AMBER), (0.40, 0.52, RED))):
        box(f"Taillamp_{tag}{k}", (x1 - x0 - 0.008, 0.012, 0.145),
            loc=(s * (x0 + x1) / 2, YR + 0.02, 0.78), material=m)
    box(f"Reverse_Lamp_{tag}", (0.10, 0.013, 0.04), loc=(s * 0.46, YR + 0.021, 0.73), material=LENS)
    wrap = box(f"Taillamp_Wrap_{tag}", (0.02, 0.13, 0.12),
               loc=(s * (surf_x(2.20, 0.78) - 0.004), 2.20, 0.78), material=RED)
    bevel(wrap, 0.006, 2)
garn = box("Rear_Garnish", (0.78, 0.016, 0.12), loc=(0, YR + 0.006, 0.78), material=TRIM)
box("Rear_Garnish_Trim", (0.78, 0.018, 0.01), loc=(0, YR + 0.008, 0.845), material=CHROME)
box("Plate_Recess", (0.40, 0.012, 0.19), loc=(0, YR + 0.004, 0.565), material=TRIM)
box("Plate_Rear", (0.335, 0.012, 0.155), loc=(0, YR + 0.012, 0.565), material=PLATE)
box("Plate_Front", (0.335, 0.012, 0.155), loc=(0, -2.432, 0.37), material=PLATE)
sonata = text3d("Badge_SONATA", "SONATA", size=0.05, depth=0.002, loc=(-0.58, YR + 0.004, 0.655),
                rot=(math.pi / 2, 0, math.pi), material=CHROME)
pipe = cylinder("Exhaust_Pipe", r=0.028, h=0.42, loc=(0.52, 2.20, 0.235), rot=(-math.pi / 2, 0, 0),
                base=False, material=CHROME, seg=20)

# ── 9. 측면: 몰딩, 손잡이, 미러, 와이퍼 ────────────────────
for s, tag in ((1, "R"), (-1, "L")):
    for k, (y0, y1) in enumerate(((-2.26, -1.80), (-0.99, 0.86), (1.66, 2.26))):
        yc, ln = (y0 + y1) / 2, y1 - y0
        x = s * (surf_x(yc, 0.52) + 0.006)
        m = box(f"Side_Molding_{tag}{k}", (0.022, ln, 0.05), loc=(x, yc, 0.52), material=TRIM)
        bevel(m, 0.008, 2)
        box(f"Side_Molding_Chrome_{tag}{k}", (0.024, ln - 0.01, 0.007), loc=(x, yc, 0.528), material=CHROME)
    for k, y in enumerate((0.22, 1.14)):
        box(f"Door_Handle_{tag}{k}", (0.016, 0.13, 0.026), loc=(s * (surf_x(y, 0.83) + 0.004), y, 0.83),
            material=CHROME)
    # 사이드 미러
    mb = box(f"Mirror_Base_{tag}", (0.06, 0.10, 0.05), loc=(s * (side_x(0.97) + 0.02), -0.47, 0.975), material=TRIM)
    mh = box(f"Mirror_Head_{tag}", (0.13, 0.06, 0.09), loc=(s * (side_x(0.97) + 0.10), -0.46, 1.0),
             rot=(0, 0, s * math.radians(-6)), material=TRIM)
    bevel(mh, 0.015, 3)
    box(f"Mirror_Glass_{tag}", (0.11, 0.004, 0.07), loc=(s * (side_x(0.97) + 0.10), -0.428, 1.0),
        rot=(0, 0, s * math.radians(-6)), material=LENS)

ws_ang = math.atan2(ROOF - BELT, ROOF_Y0 - CAB_Y0)
for k, x in enumerate((-0.32, 0.22)):
    z = 0.975
    n = Vector((0, -math.sin(ws_ang), math.cos(ws_ang)))
    p = Vector((x, a_line(z), z)) + n * 0.014
    box(f"Wiper_{k}", (0.50, 0.012, 0.012), loc=p, rot=(ws_ang, 0, math.radians(4)), material=TRIM)
for s, tag in ((1, "R"), (-1, "L")):
    y0, y1 = a_line(BELT) + 0.07, c_line(BELT) - 0.10
    box(f"Belt_Molding_{tag}", (0.012, y1 - y0, 0.014), loc=(s * (side_x(BELT + 0.012) + 0.004), (y0 + y1) / 2, BELT + 0.012),
        material=CHROME)
    box(f"Drip_Rail_{tag}", (0.010, ROOF_Y1 - ROOF_Y0 - 0.06, 0.012),
        loc=(s * (ROOF_HW + 0.002), (ROOF_Y0 + ROOF_Y1) / 2, ROOF - 0.022), material=CHROME)
box("Cowl_Panel", (1.50, 0.12, 0.012), loc=(0, a_line(BELT) - 0.06, surf_z(0, -0.72) + 0.002), material=TRIM)


# ── 10. 마무리: 커브·글자를 메시로 바꾸고, 앞뒤 오버행을 실측 전장에 맞춘다 ──
def to_mesh(ob):
    dg = bpy.context.evaluated_depsgraph_get()
    me = bpy.data.meshes.new_from_object(ob.evaluated_get(dg))
    new = bpy.data.objects.new(ob.name, me)
    link(new)
    new.matrix_world = ob.matrix_world.copy()
    bpy.data.objects.remove(ob, do_unlink=True)
    return new


for ob in list(bpy.context.scene.objects):
    if ob.type in ('CURVE', 'FONT'):
        to_mesh(ob)

# 차체는 범퍼 끝까지 4.68m 가 되도록 앞뒤 바퀴 바깥(|y|>1.8)만 압축한다.
OH0, OH_K = 1.8, 0.852


def fit_y(y):
    a = abs(y)
    return y if a <= OH0 else math.copysign(OH0 + (a - OH0) * OH_K, y)


for ob in list(bpy.context.scene.objects):
    if ob.type != 'MESH' or ob.name.startswith("Wheel") or ob.get('atelier_cutter'):
        continue
    if ob.data.users > 1:
        ob.data = ob.data.copy()
    mw = ob.matrix_world.copy()
    inv = mw.inverted()
    for v in ob.data.vertices:
        w = mw @ v.co
        w.y = fit_y(w.y)
        v.co = inv @ w
