"""Генератор страниц раздела «Архитектура»: список /arch/ и страницы проектов /arch/<проект>/. Запуск из корня репозитория: python3 tools/gen_arch.py Содержание проектов (тексты, картинки, подписи) лежит в списке CASES ниже.""" import os, html W = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'web') + '/' E = html.escape def head(title, desc, og, depth=1): return f''' {E(title)}: Аман Ибраш ''' TAIL = ''' ''' # ---------- данные проектов ---------- CASES = [ dict(slug='quiet-courtyard', title='Тихий двор', sub='Дом в Конаеве вокруг внутреннего двора', year='2026', place='Конаев', kind='жилой дом', cover='house-pond', og='arch-quiet-courtyard.jpg', card='Бетон, дерево и двор, в который приходят небо и свет.', desc='Тихий двор: одноэтажный бетонный дом в Конаеве вокруг внутреннего двора. Проект в диалоге с архитектурой Тадао Андо.', intro='Дом, который отвернулся от улицы и повернулся к небу.', stats=[('1','этаж',''),('32','м² внутренний двор',''),('41','м² гостиная-кухня',''),('3','спальни','')], sections=[ ('Улица остаётся снаружи', 'Со стороны улицы это глухая бетонная стена с одной узкой щелью света. Она прячет вход от чужих глаз: чтобы попасть в дом, нужно пройти вдоль сада и только потом повернуть к двери. Рядом под бетонным навесом стоит машина, а сам дом остаётся спокойным и закрытым.'), ('Всё собрано вокруг двора', 'Внутри всё держится на дворе площадью 32 м². Утреннее солнце заходит в него, галерея обходит двор по кругу, а комнаты открываются на гравий, воду и тонкие деревья. Над центром плана стоит зенитный фонарь: он приносит верхний свет в самую глубину дома.'), ('Бетон и тёплое дерево', 'Главный материал один: бетон с отпечатками опалубки. Рядом с ним тёплое дерево дверей и потолочных обшивок, чёрный металл рам и зелень. Проект сделан в диалоге с архитектурой Тадао Андо: меньше деталей, больше света, тишины и тени.')], gallery=[('house-street','Фасад с улицы: глухая стена и въезд под навес','',1200,896), ('house-courtyard','Двор: стекло, дерево и гравий','',1200,896), ('house-pond','Фасад в сад: бетон, вода и зелень','',1200,896), ('house-gallery','Галерея вокруг двора','',1200,896), ('house-corridor','Коридор вдоль двора','',1200,896), ('house-entry','Вход: свет и тени на бетоне','',1200,896)], drawings=[('house-concept','Путь и свет: вход с улицы, щель света, путь вдоль сада, зенитный фонарь и утреннее солнце во двор','portrait',1070,1200), ('house-plan','План первого этажа с зонированием','portrait',1040,1440), ('house-axo','Аксонометрия со срезом','wide',1400,1088)]), dict(slug='exhibition-pavilion', title='Выставочный павильон', sub='Галерея в парке', year='2026', place='Алматы', kind='общественное здание', cover='pav-2', og='arch-exhibition-pavilion.jpg', card='Стеклянная витрина среди деревьев и терраса для скульптур.', desc='Выставочный павильон в Алматы: двухэтажная галерея в парке со стеклянным первым этажом и террасой для скульптур.', intro='Витрина, которую видно с аллеи ещё до входа.', stats=[('2','этажа',''),('26','м длина здания',''),('2','галереи',''),('1','терраса для скульптур','')], sections=[ ('Первый этаж как витрина', 'Павильон стоит в парке и работает как витрина: первый этаж почти целиком остеклён, поэтому экспозицию видно с аллеи ещё до входа. Входишь, и сразу попадаешь в холл, из которого открываются обе галереи.'), ('Спокойный план', 'Из холла гости идут в две галереи, а лестница ведёт на второй уровень с открытой террасой для скульптур. Служебные помещения собраны в одном крыле, поэтому залы остаются свободными и чистыми для выставок.'), ('Три материала', 'Фасад собран из трёх материалов: стеклянная витрина, плоскости бетона и тёплые деревянные ламели. Они держат один спокойный ритм и не спорят с парком вокруг.')], gallery=[('pav-2','Главный фасад','',1600,896),('pav-1','Вход и холл','',1600,893),('pav-3','Вид сверху на парк','',1600,893)], drawings=[('pav-plan','План первого этажа: две галереи и холл','wide',1600,844)]), dict(slug='loft-complex', title='Жилой комплекс «Лофт»', sub='Три корпуса вокруг двора без машин', year='2026', place='Алматы', kind='жилой комплекс', cover='loft-0', og='arch-loft-complex.jpg', card='Красный кирпич и общий двор, свободный от машин.', desc='Жилой комплекс «Лофт» в Алматы: три корпуса из красного кирпича вокруг общего двора без машин.', intro='Красный кирпич, арки на первом этаже и двор, где можно гулять.', stats=[('3','корпуса',''),('2,11','га участок',''),('7','этажей, корпус 1',''),('1','подземный паркинг','')], sections=[ ('Двор без машин', 'Комплекс стоит на участке 2,11 га в Турксибском районе Алматы. Три корпуса образуют общий двор, и машин в нём нет: парковка спрятана под землёй, а на поверхности остались площадки и озеленение.'), ('Кирпич и арки', 'На первом этаже коммерция за арочными стальными витражами. Выше идут жилые этажи с окнами в стальных рамах. Фасад корпуса 1 выполнен из красного лицевого кирпича, а стальной пояс отделяет коммерческий этаж от квартир.')], gallery=[('loft-0','Общий вид комплекса','',1600,1198)], drawings=[('loft-facade','Боковые фасады корпуса 1','wide',1600,877)]), ] import subprocess def dims(img): o = subprocess.run(['magick', 'identify', '-format', '%w %h', W + f'img/arch/{img}.webp'], capture_output=True, text=True).stdout.split() return int(o[0]), int(o[1]) def srcset(img): w, _ = dims(img) parts = [] for suf, ww in (('-sm', 640), ('-md', 960)): if os.path.exists(W + f'img/arch/{img}{suf}.webp') and ww < w: parts.append(f'/img/arch/{img}{suf}.webp {ww}w') parts.append(f'/img/arch/{img}.webp {w}w') return ', '.join(parts) def fig(img, cap, kind, w_unused, h_unused, lead=False, sizes='(min-width: 62rem) 62rem, calc(100vw - 2.2rem)'): w, h = dims(img) cls = 'shot' + (f' shot--{kind}' if kind else '') + (' shot--lead' if lead else '') lz = ' fetchpriority="high"' if lead else ' loading="lazy" decoding="async" fetchpriority="low"' return f'''
{E(cap)}
{E(cap)}
''' # ---------- страницы проектов ---------- for i, c in enumerate(CASES): nxt = CASES[(i + 1) % len(CASES)] cover = next(g for g in c['gallery'] if g[0] == c['cover']) rest = [g for g in c['gallery'] if g[0] != c['cover']] meta = ''.join(f'
  • {E(x)}
  • ' for x in (c['year'], c['place'], c['kind'])) stats = ''.join(f'
  • {n}{E(l)}
  • ' for n, l, _ in c['stats']) secs = ''.join(f'

    {E(t)}

    {E(p)}

    ' for t, p in c['sections']) gal = ''.join(fig(g[0], g[1], g[2], g[3], g[4]) for g in rest) dr = ''.join(fig(g[0], g[1], g[2], g[3], g[4]) for g in c['drawings']) h = head(c['title'], c['desc'], c['og']) h += f'''

    ← Архитектура

    {E(c['title'])}

    {E(c['sub'])}

    {E(c['intro'])}

    {secs}
    ''' if rest: h += f'''

    Кадры

    ''' h += f'''

    Чертежи и схемы

    Следующий проект {E(nxt['title'])}
    ''' + TAIL os.makedirs(W + f"arch/{c['slug']}", exist_ok=True) open(W + f"arch/{c['slug']}/index.html", 'w').write(h) # ---------- список проектов ---------- cards = '' for i, c in enumerate(CASES): lead = ' work--lead' if i == 0 else '' cards += f'''
    {E(c['title'])}

    {E(c['title'])}

    {E(c['card'])}

    Смотреть проект
    ''' idx = head('Архитектура', 'Учусь архитектуре в KazGASA, Алматы. Мои лучшие проекты: Тихий двор, выставочный павильон и жилой комплекс.', 'arch.jpg') idx += f'''

    Архитектура

    Рисую здания

    Я студент KazGASA в Алматы. Здесь проекты, которыми горжусь больше всего. Выбери любой и загляни внутрь.

    {cards}
    ''' + TAIL open(W + 'arch/index.html', 'w').write(idx) # sitemap: дописываем недостающие адреса, чужие не трогаем sm_path = W + 'sitemap.xml' sm = open(sm_path).read() if os.path.exists(sm_path) else '\n\n\n' for u in ['/', '/arch/'] + [f"/arch/{c['slug']}/" for c in CASES]: if f'https://me.ibrash.duckdns.org{u}' not in sm: sm = sm.replace('', f' https://me.ibrash.duckdns.org{u}\n') open(sm_path, 'w').write(sm) print('arch pages:', [c['slug'] for c in CASES])