# 타워 브리지 (Tower Bridge, 런던, 1894) — Blender Python 으로 코드만 써서 만든 모델.
# 다리 축은 X, 템스강 흐름은 Y 방향. 강바닥(교각 밑면)이 z=0, 실제 크기(미터) 그대로다.
# 두 주탑은 석재 블록 + 팔각 모서리 탑 + 피라미드 지붕으로 쌓고, 아치·고딕 창은 불리언으로 파냈다.
# 고가 보도·현수 체인의 트러스는 bmesh 로 각재(beam)를 하나하나 이어 붙여 만들었고,
# 체인 곡선은 포물선 식으로 계산했다. 한쪽 주탑·교대탑을 만든 뒤 링크 복제로 반대편에 돌려 놓았다.
from atelier import *
from atelier import _mesh_obj

SHOT = {"azimuth": -62, "elevation": 11, "lens": 50, "light": "day", "background": "#26313f"}

# ── 재질 ────────────────────────────────────────────────
STONE = mat('Portland stone', '#cdc3ab', 0.75)
GRANITE = mat('Cornish granite', '#8f8c86', 0.8)
SLATE = mat('Slate roof', '#4b5058', 0.55, 0.15)
BLUE = mat('Bridge blue paint', '#5f97c8', 0.4, 0.2)
WHITE = mat('Bridge white paint', '#e4e2da', 0.4, 0.1)
ROAD = mat('Road asphalt', '#3a3c3f', 0.9)
WIN = mat('Window glass dark', '#232a33', 0.15, 0.3)
GOLD = mat('Gilded finial', '#c9a24a', 0.3, 1.0)

# ── 주요 치수 ───────────────────────────────────────────
TX = 39.0          # 주탑 중심 x (두 주탑 안쪽 면 사이 = 중앙 경간 약 62m)
THX, THY = 8.0, 11.0   # 주탑 몸체 반폭
TBASE = 6.0        # 교각 위 탑 시작 높이
TTOP = 50.0        # 탑 몸체 처마 높이
DECK = 9.5         # 노면 높이
DECKW = 8.0        # 노면 반폭
AX = 121.0         # 교대탑(강변 작은 탑) 중심 x
WY = 8.0           # 보도·체인 중심 y
WZ0, WZ1 = 42.0, 47.0  # 고가 보도 높이


# ── bmesh 각재 빌더 ─────────────────────────────────────
class MB:
    def __init__(self):
        self.bm = bmesh.new()

    def beam(self, p0, p1, w, h=None):
        h = h or w
        p0, p1 = Vector(p0), Vector(p1)
        d = p1 - p0
        if d.length < 1e-6:
            return
        f = d.normalized()
        up = Vector((0, 0, 1)) if abs(f.z) < 0.95 else Vector((0, 1, 0))
        s = f.cross(up).normalized()
        u = s.cross(f).normalized()
        vs = []
        for p in (p0, p1):
            for a, b in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
                vs.append(self.bm.verts.new(p + s * (a * w / 2) + u * (b * h / 2)))
        q = [(0, 1, 2, 3), (7, 6, 5, 4), (0, 4, 5, 1), (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0)]
        for f_ in q:
            self.bm.faces.new([vs[i] for i in f_])

    def box(self, c, size):
        self.beam((c[0] - size[0] / 2, c[1], c[2]), (c[0] + size[0] / 2, c[1], c[2]), size[1], size[2])

    def obj(self, name, material):
        bmesh.ops.recalc_face_normals(self.bm, faces=self.bm.faces)
        me = bpy.data.meshes.new(name)
        self.bm.to_mesh(me)
        self.bm.free()
        ob = bpy.data.objects.new(name, me)
        link(ob)
        return assign(ob, material)


def lattice(mb, bot, top, w, chord_w=None, web=None, diag=True):
    """bot/top: 같은 개수의 점 열. 위·아래 현재, 수직재, X자 사재."""
    cw = chord_w or w * 1.6
    web = web if web is not None else mb
    for i in range(len(bot) - 1):
        mb.beam(bot[i], bot[i + 1], cw, cw)
        mb.beam(top[i], top[i + 1], cw, cw)
        if diag:
            web.beam(bot[i], top[i + 1], w, w)
            web.beam(top[i], bot[i + 1], w, w)
    for i in range(len(bot)):
        web.beam(bot[i], top[i], w * 1.2, w * 1.2)


def pointed_arch(w, z0, hs, rr=0.7, n=10):
    """폭 w 의 뾰족 아치 단면 [(y,z)…]: z0 바닥, hs 아치 시작 높이."""
    R = rr * w
    c = R - w / 2
    top = math.sqrt(R * R - c * c)
    a_end = math.pi - math.atan2(top, c)  # 왼쪽 호: 중심 (+c, hs), 꼭짓점 y=0
    pts = [(-w / 2, z0), (-w / 2, hs)]
    for i in range(1, n + 1):
        a = math.pi - (math.pi - a_end) * i / n
        pts.append((c + R * math.cos(a), hs + R * math.sin(a)))
    for i in range(n - 1, -1, -1):
        a = math.pi - (math.pi - a_end) * i / n
        pts.append((-c - R * math.cos(a), hs + R * math.sin(a)))
    pts.append((w / 2, z0))
    return pts


def arch_prism(name, w, z0, hs, depth, rr=0.7):
    """Y 축 방향으로 돌출한 뾰족 아치 기둥(단면은 XZ)."""
    return extrude_profile(name, pointed_arch(w, z0, hs, rr), depth)


def apply_cut(target, cutters):
    cut = join(cutters, name=target.name + ' cutter') if len(cutters) > 1 else cutters[0]
    boolean(target, cut)
    apply_all(target)
    bpy.data.objects.remove(cut, do_unlink=True)
    return target


def merge(parts, name, material):
    """부품을 합치고 변환을 메시에 구워 원점을 월드 원점(탑 중심축)에 둔다."""
    o = join(parts, name=name)
    o.data.transform(o.matrix_world)
    o.matrix_world = Matrix.Identity(4)
    assign(o, material)
    for p in o.data.polygons:
        p.material_index = 0
    return o


def pyramid(name, hx, hy, h, z, ridge=0.0):
    """직사각형 밑면(반폭 hx, hy)의 지붕. ridge>0 이면 X 방향 용마루가 있는 모임지붕."""
    bm = bmesh.new()
    b = [bm.verts.new(c) for c in ((-hx, -hy, z), (hx, -hy, z), (hx, hy, z), (-hx, hy, z))]
    if ridge > 0:
        t = [bm.verts.new((-ridge, 0, z + h)), bm.verts.new((ridge, 0, z + h))]
        for f in ((b[0], b[1], t[1], t[0]), (b[1], b[2], t[1]), (b[2], b[3], t[0], t[1]), (b[3], b[0], t[0])):
            bm.faces.new(f)
    else:
        t = bm.verts.new((0, 0, z + h))
        for i in range(4):
            bm.faces.new((b[i], b[(i + 1) % 4], t))
    bm.faces.new(list(reversed(b)))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return _mesh_obj(name, bm, (0, 0, 0), (0, 0, 0), None)


def octa(name, r, h, z, x, y, material, r2=None):
    return cylinder(name, r, h, loc=(x, y, z), rot=(0, 0, math.pi / 8), material=material, seg=8, r2=r2)


# ── 주탑 ────────────────────────────────────────────────
def build_main_tower():
    parts_stone, parts_slate, parts_gold, parts_win, parts_gran = [], [], [], [], []
    # 화강암 교각 (강물 흐름 방향 Y 로 뾰족한 배 모양)
    pier = prism('Pier', [(-12, -17), (0, -23), (12, -17), (12, 17), (0, 23), (-12, 17)], TBASE)
    parts_gran.append(pier)
    parts_gran.append(box('Pier cap', (24.6, 34.6, 0.6), loc=(0, 0, TBASE - 0.3)))
    # 몸체
    body = box('Body', (2 * THX, 2 * THY, TTOP - TBASE), loc=(0, 0, TBASE), base=True)
    cutters = []
    road = arch_prism('road arch', 2 * DECKW, DECK - 0.5, 15.0, 2 * THY + 2, rr=0.72)
    road.rotation_euler.z = math.pi / 2
    road.scale.x = 1.0
    # extrude_profile 은 XZ 단면을 Y 로 돌출 → Z 회전 90° 로 X 방향 터널
    cutters.append(road)
    cutters[-1].scale = (1, (2 * THX + 2) / (2 * THY + 2), 1)
    # 강 쪽 면(±Y)의 고딕 창
    win_panels = []
    for sy in (-1, 1):
        for zc, hgt, ww, xs in ((13.0, 6.0, 1.6, (-3.2, 0, 3.2)), (24.0, 7.0, 1.6, (-3.2, 0, 3.2)),
                                (33.0, 6.0, 1.4, (-3.0, -1.0, 1.0, 3.0)), (43.5, 4.0, 1.4, (-2.0, 0, 2.0))):
            for xw in xs:
                c = arch_prism('win', ww, 0, hgt - ww * 0.9, 1.2, rr=0.75)
                c.location = (xw, sy * THY, zc)
                cutters.append(c)
                p = arch_prism('pane', ww, 0, hgt - ww * 0.9, 0.06, rr=0.75)
                p.location = (xw, sy * (THY - 0.5), zc)
                win_panels.append(p)
    # 다리 쪽 면(±X)의 창(도로 아치 위)
    for sx in (-1, 1):
        for zc, hgt, ys in ((27.5, 6.0, (-3.0, 0, 3.0)), (35.0, 5.0, (-2.0, 0, 2.0))):
            for yw in ys:
                c = arch_prism('win', 1.5, 0, hgt - 1.3, 1.2, rr=0.75)
                c.rotation_euler.z = math.pi / 2
                c.location = (sx * THX, yw, zc)
                cutters.append(c)
                p = arch_prism('pane', 1.5, 0, hgt - 1.3, 0.06, rr=0.75)
                p.rotation_euler.z = math.pi / 2
                p.location = (sx * (THX - 0.5), yw, zc)
                win_panels.append(p)
    apply_cut(body, cutters)
    parts_stone.append(body)
    parts_win += win_panels
    # 띠 몰딩·처마·난간
    for z, t in ((DECK + 1.5, 0.6), (21.0, 0.5), (31.0, 0.5), (40.5, 0.6), (TTOP - 0.6, 0.9)):
        parts_stone.append(box('course', (2 * THX + 0.6, 2 * THY + 0.6, t), loc=(0, 0, z), base=True))
    # 처마 위 흉벽(파라펫) — 네 변
    for sy in (-1, 1):
        parts_stone.append(box('parapet', (2 * THX, 0.5, 1.4), loc=(0, sy * (THY - 0.25), TTOP), base=True))
    for sx in (-1, 1):
        parts_stone.append(box('parapet', (0.5, 2 * THY, 1.4), loc=(sx * (THX - 0.25), 0, TTOP), base=True))
    # 흉벽 가운데 작은 첨탑
    for sx, sy in ((0, 1), (0, -1), (1, 0), (-1, 0)):
        px, py = sx * (THX - 0.25), sy * (THY - 0.25)
        parts_stone.append(octa('parapet pinnacle', 0.5, 2.8, TTOP, px, py, None))
        parts_slate.append(octa('pinnacle cap', 0.6, 1.6, TTOP + 2.8, px, py, None, r2=0.0))
    # 화강암 기단(노면 아래)
    parts_gran.append(box('plinth', (2 * THX + 0.8, 2 * THY + 0.8, DECK - TBASE), loc=(0, 0, TBASE), base=True))
    # 모서리 팔각탑
    for sx in (-1, 1):
        for sy in (-1, 1):
            x, y = sx * THX, sy * THY
            parts_gran.append(octa('turret base', 3.0, DECK - TBASE, TBASE, x, y, None))
            parts_stone.append(octa('turret', 2.6, 54.0 - DECK, DECK, x, y, None))
            for z in (DECK + 1.5, 31.0, 40.5, 53.0):
                parts_stone.append(octa('turret ring', 2.85, 0.6, z, x, y, None))
            # 팔각탑 바깥 세 면의 좁은 창
            ap = 2.6 * math.cos(math.pi / 8)
            base_a = math.atan2(sy, sx)
            for da in (-math.pi / 4, 0.0, math.pi / 4):
                a = base_a + da
                for zc in (17.0, 25.5, 35.0, 44.5):
                    pn = box('turret slit', (0.08, 0.7, 2.6), loc=(x + (ap + 0.03) * math.cos(a), y + (ap + 0.03) * math.sin(a), zc),
                             rot=(0, 0, a), base=True)
                    parts_win.append(pn)
            parts_slate.append(octa('turret spire', 2.95, 12.0, 54.0, x, y, None, r2=0.0))
            parts_gold.append(cylinder('finial', 0.18, 2.2, loc=(x, y, 65.6), seg=8))
            parts_gold.append(sphere('finial ball', 0.38, loc=(x, y, 65.9), seg=12, rings=6))
    # 중앙 피라미드 지붕 + 랜턴
    parts_slate.append(pyramid('main roof', THX - 0.6, THY - 0.6, 12.0, TTOP))
    # 지붕 도머(박공창)
    for sy in (-1, 1):
        g = extrude_profile('dormer', [(-2.2, 0), (2.2, 0), (2.2, 3.0), (0, 5.5), (-2.2, 3.0)], 3.0)
        g.location = (0, sy * (THY - 2.2), TTOP)
        parts_stone.append(g)
        cap = extrude_profile('dormer roof', [(-2.6, 2.8), (0, 5.8), (2.6, 2.8), (2.6, 3.3), (0, 6.3), (-2.6, 3.3)], 3.4)
        cap.location = (0, sy * (THY - 2.2), TTOP)
        parts_slate.append(cap)
    parts_stone.append(octa('lantern', 1.3, 3.0, 58.5, 0, 0, None))
    parts_slate.append(octa('lantern spire', 1.55, 5.0, 61.5, 0, 0, None, r2=0.0))
    parts_gold.append(cylinder('main finial', 0.2, 2.2, loc=(0, 0, 66.0), seg=8))

    out = []
    for parts, m, nm in ((parts_stone, STONE, 'Main tower stonework'), (parts_gran, GRANITE, 'Main tower granite pier'),
                         (parts_slate, SLATE, 'Main tower roofs'), (parts_gold, GOLD, 'Main tower finials'),
                         (parts_win, WIN, 'Main tower windows')):
        out.append(merge(parts, nm, m))
    return out


def build_abutment_tower():
    hx, hy, top = 5.5, 10.0, 22.0
    stone, slate, gran, win = [], [], [], []
    body = box('Abut body', (2 * hx, 2 * hy, top), base=True)
    road = arch_prism('abut arch', 2 * DECKW, DECK - 0.5, 11.5, 2 * hx + 2, rr=0.7)
    road.rotation_euler.z = math.pi / 2
    cut = [road]
    for sy in (-1, 1):
        for xw in (-1.8, 1.8):
            c = arch_prism('win', 1.4, 0, 3.6, 1.2)
            c.location = (xw, sy * hy, 14.0)
            cut.append(c)
            p = arch_prism('pane', 1.4, 0, 3.6, 0.06)
            p.location = (xw, sy * (hy - 0.5), 14.0)
            win.append(p)
    apply_cut(body, cut)
    stone.append(body)
    gran.append(box('abut base', (2 * hx + 1.0, 2 * hy + 1.0, DECK - 1.5), base=True))
    for z, t in ((DECK + 1.2, 0.5), (top - 0.6, 0.8)):
        stone.append(box('course', (2 * hx + 0.5, 2 * hy + 0.5, t), loc=(0, 0, z), base=True))
    for sx in (-1, 1):
        for sy in (-1, 1):
            x, y = sx * hx, sy * hy
            stone.append(octa('pinnacle', 1.4, top + 2.5, 0, x, y, None))
            slate.append(octa('pinnacle spire', 1.65, 5.0, top + 2.5, x, y, None, r2=0.0))
    slate.append(pyramid('abut roof', hx - 0.3, hy - 0.3, 7.5, top, ridge=0.0))
    out = []
    for parts, m, nm in ((stone, STONE, 'Abutment tower stonework'), (gran, GRANITE, 'Abutment tower base'),
                         (slate, SLATE, 'Abutment tower roofs'), (win, WIN, 'Abutment tower windows')):
        out.append(merge(parts, nm, m))
    return out


# ── 노면 ────────────────────────────────────────────────
def build_deck():
    # 중앙 도개교 두 짝(가운데 이음매)
    for sx in (-1, 1):
        x0, x1 = 0.06, TX - THX + 0.5
        cx = sx * (x0 + x1) / 2
        # 가운데 쪽 2.4m, 탑 쪽 4.8m 로 깊어지는 도개교 잎 단면
        prof = [(x0, DECK), (x0, DECK - 2.4), (x1 - 6.0, DECK - 3.4), (x1, DECK - 4.8), (x1, DECK)]
        if sx < 0:
            prof = [(-px, pz) for px, pz in reversed(prof)]
        extrude_profile('Bascule leaf ' + ('south' if sx < 0 else 'north'), prof, 2 * DECKW, material=BLUE)
        top = box('Bascule roadway', (x1 - x0, 2 * DECKW - 1.0, 0.04), loc=(cx, 0, DECK), base=True, material=ROAD)
    # 측경간 노면(주탑 ~ 교대탑)
    for sx in (-1, 1):
        x0, x1 = TX + THX - 0.5, AX - 5.0
        cx = sx * (x0 + x1) / 2
        box('Side span girder', (x1 - x0, 2 * DECKW, 2.0), loc=(cx, 0, DECK - 2.0), base=True, material=BLUE)
        box('Side span roadway', (x1 - x0, 2 * DECKW - 1.0, 0.04), loc=(cx, 0, DECK), base=True, material=ROAD)
    # 접속 노면(교대탑 바깥)
    for sx in (-1, 1):
        x0, x1 = AX + 5.0, AX + 12.0
        cx = sx * (x0 + x1) / 2
        # 석조 옹벽 + 화강암 기단 + 양쪽 흉벽
        box('Approach granite footing', (x1 - x0, 2 * DECKW + 2.4, 2.5), loc=(cx, 0, 0), base=True, material=GRANITE)
        box('Approach retaining wall', (x1 - x0, 2 * DECKW + 2, DECK - 2.5), loc=(cx, 0, 2.5), base=True, material=STONE)
        box('Approach roadway', (x1 - x0, 2 * DECKW - 1.0, 0.04), loc=(cx, 0, DECK), base=True, material=ROAD)
        for sy in (-1, 1):
            box('Approach parapet', (x1 - x0, 1.0, 1.2), loc=(cx, sy * (DECKW + 0.5), DECK), base=True, material=STONE)
            box('Approach coping', (x1 - x0, 1.2, 0.25), loc=(cx, sy * (DECKW + 0.5), DECK + 1.2), base=True, material=GRANITE)
    # 난간 (흰 기둥 + 파란 난간대)
    mb = MB()
    for sy in (-1, 1):
        y = sy * (DECKW - 0.25)
        for seg in ((-(TX - THX), TX - THX), (TX + THX, AX - 5.0), (-(AX - 5.0), -(TX + THX))):
            a, b = seg
            n = max(2, int(abs(b - a) / 2.5))
            mb.beam((a, y, DECK + 1.2), (b, y, DECK + 1.2), 0.18, 0.18)
            mb.beam((a, y, DECK + 0.25), (b, y, DECK + 0.25), 0.14, 0.14)
            for i in range(n + 1):
                x = a + (b - a) * i / n
                mb.beam((x, y, DECK), (x, y, DECK + 1.3), 0.14, 0.14)
    mb.obj('Deck railings', WHITE)
    # 가로등(난간 위 기둥 + 등)
    posts, lamps = MB(), []
    for sy in (-1, 1):
        y = sy * (DECKW - 0.25)
        for x in (-22.0, -11.0, 11.0, 22.0, 60.0, 75.0, 90.0, 105.0, -60.0, -75.0, -90.0, -105.0):
            posts.beam((x, y, DECK), (x, y, DECK + 4.2), 0.22, 0.22)
            lamps.append(sphere('lamp', 0.38, loc=(x, y, DECK + 4.5), seg=10, rings=6))
    posts.obj('Lamp posts', BLUE)
    merge(lamps, 'Lamp globes', mat('Lamp glass', '#efe3c2', 0.3))


# ── 고가 보도 ───────────────────────────────────────────
def build_walkways():
    x0 = TX - THX
    n = 20
    blue, white = MB(), MB()
    for sy in (-1, 1):
        yc = sy * WY
        for side in (-1.6, 1.6):
            y = yc + side
            bot = [(-x0 + 2 * x0 * i / n, y, WZ0) for i in range(n + 1)]
            top = [(-x0 + 2 * x0 * i / n, y, WZ1) for i in range(n + 1)]
            lattice(white, bot, top, 0.22, chord_w=0.6, web=blue)
        # 바닥·지붕판
        white.box((0, yc, WZ0 - 0.2), (2 * x0, 3.6, 0.4))
        blue.box((0, yc, WZ1 + 0.25), (2 * x0, 3.8, 0.5))
        # 아래쪽 까치발 곡선 브래킷(탑에서 나오는 부분)
        for sx in (-1, 1):
            for side in (-1.6, 1.6):
                pts = []
                for k in range(9):
                    t = k / 8
                    ang = t * math.pi / 2
                    pts.append((sx * (x0 - 5.0 * (1 - math.cos(ang))), yc + side, WZ0 - 4.0 * (1 - math.sin(ang))))
                for k in range(8):
                    blue.beam(pts[k], pts[k + 1], 0.45, 0.45)
                for k in range(1, 8):
                    blue.beam(pts[k], (pts[k][0], pts[k][1], WZ0), 0.15, 0.15)
    blue.obj('High-level walkways lattice', BLUE)
    white.obj('High-level walkways chords', WHITE)


# ── 현수 체인(측경간) ──────────────────────────────────
def chain_bottom(x):
    xm = 95.0
    if x <= xm:
        return 11.2 + (WZ0 - 11.2) * ((xm - x) / (xm - (TX + THX))) ** 2
    return 11.2 + 5.6 * ((x - xm) / (AX - 5.0 - xm)) ** 2


def chain_depth(x):
    xm = 95.0
    if x <= xm:
        return 1.3 + 3.7 * ((xm - x) / (xm - (TX + THX))) ** 2
    return 1.3 + 1.5 * ((x - xm) / (AX - 5.0 - xm)) ** 2


def build_chains():
    blue, white, hang = MB(), MB(), MB()
    xa, xb = TX + THX, AX - 5.0
    n = 22
    for sx in (-1, 1):
        for sy in (-1, 1):
            y = sy * WY
            xs = [xa + (xb - xa) * i / n for i in range(n + 1)]
            for dy in (-0.9, 0.9):
                bot = [(sx * x, y + dy, chain_bottom(x)) for x in xs]
                top = [(sx * x, y + dy, chain_bottom(x) + chain_depth(x)) for x in xs]
                lattice(blue, bot, top, 0.22, chord_w=0.85, web=white)
            for i in range(n + 1):
                x = xs[i]
                zb = chain_bottom(x)
                # 두 판을 잇는 가로재
                blue.beam((sx * x, y - 0.9, zb), (sx * x, y + 0.9, zb), 0.3, 0.3)
                blue.beam((sx * x, y - 0.9, zb + chain_depth(x)), (sx * x, y + 0.9, zb + chain_depth(x)), 0.3, 0.3)
                if 0 < i < n and zb - DECK > 1.6:
                    hang.beam((sx * x, y, DECK + 0.02), (sx * x, y, zb), 0.28, 0.28)
    blue.obj('Suspension chains lattice', BLUE)
    white.obj('Suspension chains chords', WHITE)
    hang.obj('Suspension hangers', BLUE)


# ── 조립 ────────────────────────────────────────────────
for o in build_main_tower():
    o.location.x = TX
    o.name = 'North ' + o.name
    d = duplicate(o, loc=(-TX, 0, o.location.z), rot=(0, 0, math.pi), name=o.name.replace('North', 'South'))
for o in build_abutment_tower():
    o.location.x = AX
    o.name = 'North ' + o.name
    duplicate(o, loc=(-AX, 0, o.location.z), rot=(0, 0, math.pi), name=o.name.replace('North', 'South'))
build_deck()
build_walkways()
build_chains()
