# Design ## Context См. proposal.md — Why. См. сценарии в specs/ui-workspace/spec.md. Опорные факты по коду: - `Viewer.svelte` — единая область просмотра: `scroller` c `` (плоский зум/панорамирование, `Space`, колесо), топбар, лента миниатюр, контекстное меню, `f`/fullscreen. Используется и в маршруте фото, и на публичной share-странице. - Теги персональные (`tags.user_id`); `photo.tags` приходят и в списке, и в `/photos/:id`. На share-страницах `GetPublic` читает фото с пустым пользователем → тегов нет. - Варианты: `thumb` 200 и `preview` 1200 (webp); плюс `original_url` (подписанный, 1 час). - В проекте нет runtime-зависимостей; статика вшивается в Go-бинарник. ## Goals / Non-Goals **Goals:** - 360-обзор для фото с личным тегом `pano360`, без сторонних библиотек. - Сохранить плоский просмотр и все прочие функции Viewer без регрессий. - Деградация до плоского изображения, когда 360 невозможен. **Non-Goals:** - Бэкенд, новый pano-вариант, автоопределение GPano/XMP. - 360 на share-страницах и у других участников (теги персональные). - Редактор панорам, горячие точки, стерео/VR. ## Decisions ### 1. Детект: личный тег `pano360` `isPanoTag(photo)` — `photo.tags?.some(t => t.name.trim().toLowerCase() === 'pano360')`. Источник — уже отдаваемые теги; бэкенд не трогаем. Следствие: на share и у других участников режим не включается (осознанное ограничение, покрыто сценарием спеки). ### 2. Рендер: мини-WebGL equirect без зависимостей Отдельный `PanoViewer.svelte`, владеющий ``: ``` экран (uv) --inv(fov,yaw,pitch)--> луч dir --atan2/asin--> lon/lat --> equirect uv --> sample ``` - Вершинный шейдер: полноэкранный quad. - Фрагментный: направление луча из `uv` + `uFov/uYaw/uPitch/uRes`; выборка текстуры equirect. - WebGL1: NPOT-текстура → `CLAMP_TO_EDGE`/`LINEAR`, без мипмапов. - `ResizeObserver` + `devicePixelRatio` для ресайза канвы. Альтернативы: three.js (быстрее писать, но +~150 КБ gz и первая runtime-зависимость) и готовая панорамная библиотека (чужая тема/UI). Отклонены в пользу 0 зависимостей. ### 3. Текстура: оригинал с даунскейлом, фолбэк preview `original_url` загружается через `fetch` того же origin (подписанный URL может быть абсолютным на хост API) → `blob` → `createImageBitmap`, чтобы пиксели были origin-clean (иначе `texImage2D` бросает «cross-origin data»). Далее даунскейл до `min(4096, gl.MAX_TEXTURE_SIZE)` и `texImage2D`. Если оригинал недоступен — `preview_url`. Новый pano-вариант не заводим (иначе бэкенд). ### 3a. Жизненный цикл WebGL GL инициализируется один раз на элемент канвы; повторные запуски эффекта (в т.ч. из-за `bind:this`) — no-op. Освобождение ресурсов (текстура/программа, `WEBGL_lose_context`) — только при уничтожении компонента. Раньше `teardown()` вызывался на перезапуске эффекта, терял контекст, и повторная инициализация компилировала шейдеры на «потерянном» контексте → ложный фолбэк. ### 4. Фолбэк на плоский `` `PanoViewer` вызывает `onFallback()` при отсутствии WebGL-контекста или сбое загрузки/декодирования; `Viewer` тогда рендерит обычный `` и плоские жесты. ### 5. Разделение ответственности и интеракции - `Viewer` владеет топбаром, лентой, клавишами и контекстным меню. - `PanoViewer` владеет канвой, указателем и колесом: drag/свайп — `yaw/pitch`, колесо — `fov` (клэмп, напр. 30°..100°). - `Space` в `Viewer` в 360-режиме вызывает `panoRef.reset()` (вместо плоского `toggleActual`). - В 360-режиме плоский зум/панорамирование и `wheel`/`pointer`-обработчики scroller не активны; свайп не листает фото (смена — стрелками и лентой); `f`/fullscreen и стрелки — как прежде. - Компонент создаётся с `key={current.id}`, ресурсы освобождаются при смене фото/размонтировании. ### 6. Индикация Признак «360» в топбаре при активном режиме; бейдж «360» на плитке сетки для фото с тегом (теги есть в списочном ответе). ## Risks / Trade-offs - [Оригинал больше лимита текстуры/много VRAM] → даунскейл до `min(4096, MAX_TEXTURE_SIZE)`. - [WebGL недоступен] → `onFallback()` и плоский ``. - [Конфликт жестов с плоским режимом] → режим-специфичные обработчики; свайп в 360 только осмотр. - [Утечки WebGL-контекста] → `deleteTexture/deleteProgram/loseContext` при размонтировании/смене фото. - [Личный тег: 360 не виден другим/на share] → покрыто сценарием спеки как ожидаемое поведение. - [Производительность на слабых GPU] → простая сцена (один quad), опционально ограничение DPR. ## Migration Plan Только фронтенд; данных и API не трогаем. Откат — удаление нового режима. Проверка: `cd frontend && npm run check` и `npm run build`, плюс панo-сценарии в CDP‑QA.