Files

91 lines
6.9 KiB
Markdown

# Design
## Context
См. proposal.md — Why. См. сценарии в specs/ui-workspace/spec.md. Опорные факты по коду:
- `Viewer.svelte` — единая область просмотра: `scroller` c `<img>` (плоский зум/панорамирование,
`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`, владеющий `<canvas>`:
```
экран (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. Фолбэк на плоский `<img>`
`PanoViewer` вызывает `onFallback()` при отсутствии WebGL-контекста или сбое загрузки/декодирования;
`Viewer` тогда рендерит обычный `<img>` и плоские жесты.
### 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()` и плоский `<img>`.
- [Конфликт жестов с плоским режимом] → режим-специфичные обработчики; свайп в 360 только осмотр.
- [Утечки WebGL-контекста] → `deleteTexture/deleteProgram/loseContext` при размонтировании/смене фото.
- [Личный тег: 360 не виден другим/на share] → покрыто сценарием спеки как ожидаемое поведение.
- [Производительность на слабых GPU] → простая сцена (один quad), опционально ограничение DPR.
## Migration Plan
Только фронтенд; данных и API не трогаем. Откат — удаление нового режима. Проверка:
`cd frontend && npm run check` и `npm run build`, плюс панo-сценарии в CDP‑QA.