
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
타워 브리지는 영국 런던 템스강에 놓인 도개교 겸 현수교로, 1886년에 착공해 1894년에 개통했다. 건축가 호러스 존스와 기술자 존 울프 배리가 설계했다. 대형 선박이 런던 항구 상류로 드나들 수 있도록 가운데 경간을 들어 올리는 도개 방식을 택했고, 이웃한 런던탑과 어울리도록 강철 골조를 석재로 감싼 빅토리아 고딕 양식의 주탑을 세웠다.
두 주탑 꼭대기는 고가 보도로 이어져 있고, 주탑과 강변의 교대탑 사이 측경간은 곡선을 그리는 현수 체인이 노면을 매달고 있다. 오늘날 강철 부재는 파란색과 흰색으로 칠해져 있으며, 이 배색은 1977년 엘리자베스 2세 즉위 25주년 때 정해졌다.
이 모델은 다리 전체를 실제 크기로 재현했다. 배 모양의 화강암 교각 위에 선 두 주탑과 네 모서리의 팔각탑, 슬레이트 첨탑과 금색 꼭지 장식, 고딕 아치 창, 격자 트러스로 짠 고가 보도, 포물선으로 늘어진 측경간 체인과 행어, 가운데에서 갈라지는 도개교 두 짝을 표현했다.
만든 방법
주탑 몸체는 상자에 뾰족 아치 단면을 돌출한 커터를 하나로 합쳐 불리언 차집합으로 도로 아치와 고딕 창을 파냈고, 창 안쪽에 어두운 유리판을 따로 넣었다. 고가 보도와 현수 체인의 트러스는 두 점 사이에 각재를 만드는 bmesh 함수로 위·아래 현재, 수직재, X자 사재를 하나씩 이어 붙였으며, 체인의 아래 현재와 깊이는 포물선 식으로 계산했다. 한쪽 주탑과 교대탑을 완성한 뒤 변환을 메시에 굽고 180° 회전한 링크 복제로 반대편에 배치했다. 석재 표면의 조각 장식, 주탑 내부 계단과 승강 기계, 도개교의 평형추 구조는 생략했고 강물 대신 교각 밑면을 바닥에 두었다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 414줄.
# 타워 브리지 (Tower Bridge, 런던, 1894) — Blender Python 으로 코드만 써서 만든 모델.# 다리 축은 X, 템스강 흐름은 Y 방향. 강바닥(교각 밑면)이 z=0, 실제 크기(미터) 그대로다.# 두 주탑은 석재 블록 + 팔각 모서리 탑 + 피라미드 지붕으로 쌓고, 아치·고딕 창은 불리언으로 파냈다.# 고가 보도·현수 체인의 트러스는 bmesh 로 각재(beam)를 하나하나 이어 붙여 만들었고,# 체인 곡선은 포물선 식으로 계산했다. 한쪽 주탑·교대탑을 만든 뒤 링크 복제로 반대편에 돌려 놓았다.from atelier import *from atelier import _mesh_objSHOT = {"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 # 교대탑(강변 작은 탑) 중심 xWY = 8.0 # 보도·체인 중심 yWZ0, 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 = []나머지 366줄 펼치기접기
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 ptsdef 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 targetdef 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 odef 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 outdef 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)) ** 2def 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)) ** 2def 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()






