chore(openspec): архивировать photo-curve-presets и обновить photos, auth
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
Сейчас кривая хранится в `light.curve` как точки на канал, `normalizeCurve`
|
||||
сортирует/зажимает/дедуплицирует их, а по умолчанию массив пуст. UI рисует
|
||||
статичные углы и прямые сегменты; `buildCurveLUT` (TS) и `curveLUT` (Go)
|
||||
интерполируют линейно. Пресетов нет. Настройки профиля лежат в
|
||||
`users.preferences` (JSON-блоб); `PATCH /auth/me/preferences` сейчас **заменяет**
|
||||
весь объект и знает только `photo_info`, поэтому добавление пресетов без мержа
|
||||
затирало бы другие разделы.
|
||||
|
||||
См. `proposal.md` — Why и `specs/photos/spec.md`, `specs/auth/spec.md` — требования.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Крайние узлы как обычные точки; первый клик вставляет между ними; узлы двигаются
|
||||
по x/y; вне них кривая плоская.
|
||||
- Плавная монотонная интерполяция в SVG-пути и в LUT; Go и TS считают одинаково.
|
||||
- Встроенные пресеты контраста и пользовательские пресеты (кривые всех четырёх
|
||||
каналов) в профиле; сохранение/применение/переименование/удаление;
|
||||
импорт/экспорт JSON.
|
||||
- Серверный мерж разделов настроек с валидацией.
|
||||
|
||||
**Non-Goals:**
|
||||
- Пресеты как «look» из четырёх кривых; Android-клиент; шаринг пресетов между
|
||||
пользователями (только файл); undo/redo.
|
||||
|
||||
## Decisions
|
||||
|
||||
**Крайние точки — обычные узлы.**
|
||||
- Первая и последняя точки массива являются крайними; по умолчанию это `(0,0)` и
|
||||
`(1,1)`. Пустой канал в UI показывает эти две точки; первое взаимодействие
|
||||
записывает их в `light.curve`.
|
||||
- Вне крайних точек значение постоянно (текущее поведение `curveEval`), поэтому
|
||||
сдвинутый по x узел даёт плоскую «полку», как на скрине.
|
||||
- При перетаскивании точки остаются отсортированными по x; UI не даёт точке
|
||||
пересечь соседнюю (x ограничивается между соседями), на коммите — `normalizeCurve`.
|
||||
- Альтернатива: фиксировать x крайних на 0/1 — отвергнуто, пользователь хочет
|
||||
двигать и по x.
|
||||
|
||||
**Интерполяция — монотонный кубический Эрмит (PCHIP).**
|
||||
- Наклоны отрезков `d_i`, касательные по Fritsch–Carlson (нулевые при смене
|
||||
знака/нулевом наклоне, ограничение против выбросов). Участок между точками
|
||||
считается по кубической формуле Эрмита.
|
||||
- SVG-путь: те же касательные переводятся в кубические Безье
|
||||
(`P1 = P0 + (h/3, m_i*h/3)`, `P2 = P3 - (h/3, m_{i+1}*h/3)`), плюс плоские
|
||||
сегменты от x=0 до первой и от последней до x=1.
|
||||
- LUT 256: те же значения; `buildCurveLUT` (TS) и `curveLUT` (Go) обязаны
|
||||
совпадать. WASM и WebGL получают готовый LUT из TS — их менять не нужно.
|
||||
- Альтернативы: Catmull-Rom (выбросы/гало на тоновой кривой), линейная
|
||||
интерполяция (текущая, отвергнута).
|
||||
|
||||
**Пресет — кривые всех четырёх каналов.**
|
||||
- `CurvePreset { name: string; channels: { rgb: CurvePoint[]; red: CurvePoint[];
|
||||
green: CurvePoint[]; blue: CurvePoint[] } }`.
|
||||
- Каждый канал содержит 2..16 точек; пустых каналов в пресете нет — линейный
|
||||
канал хранится как `(0,0),(1,1)`.
|
||||
- Встроенные `Линейный`, `Средний контраст`, `Сильный контраст` — клиентские
|
||||
константы. Контраст задаётся кривой RGB, каналы R/G/B линейные. Предлагаемые
|
||||
точки RGB:
|
||||
- Средний: `(0,0),(0.25,0.2),(0.5,0.5),(0.75,0.8),(1,1)`
|
||||
- Сильный: `(0,0),(0.2,0.12),(0.5,0.5),(0.8,0.88),(1,1)`
|
||||
- Сохранение берёт `effectiveCurvePoints` каждого канала, поэтому в пресет
|
||||
всегда попадают все четыре кривые.
|
||||
- Применение заменяет кривые всех четырёх каналов; текущий выбранный канал не
|
||||
меняется.
|
||||
- Подпись: имя пресета, если кривые всех четырёх каналов совпадают с пресетом
|
||||
(с допуском), иначе `Пользовательский`.
|
||||
|
||||
**Хранение и мерж настроек.**
|
||||
- `preferencesPayload` расширяется `curve_presets`; поля-указатели позволяют
|
||||
отличить «не передано» от «пусто».
|
||||
- Handler читает текущие `users.preferences`, мержит переданные разделы
|
||||
(`photo_info`, `curve_presets`), валидирует, пишет и возвращает полный объект.
|
||||
Сохранение «Сведений» с `{photo_info}` сохраняет пресеты; сохранение пресетов
|
||||
сохраняет `photo_info`.
|
||||
- Валидация: не больше 100 пресетов, имя 1..64, все четыре канала присутствуют
|
||||
и содержат 2..16 точек, `x,y` конечные в 0..1. Некорректный раздел
|
||||
отклоняется целиком.
|
||||
- Альтернативы: отдельный endpoint (больше поверхности), клиент шлёт весь объект
|
||||
(устаревшие вкладки затирают чужие изменения).
|
||||
|
||||
**Переключение фото.**
|
||||
- Вкладка правой панели хранится в `AlbumPanel` и передаётся в `PhotoPanel`
|
||||
через `bind:tab`, поэтому переживает перемонтирование панели при загрузке
|
||||
следующего фото.
|
||||
- Cleanup эффекта `PhotoEditPanel` регистрируется на каждом запуске; при смене
|
||||
`photo.id` накопленные правки отправляются до инициализации нового фото.
|
||||
- Ответы сохранений проверяются по id фото и номеру операции: устаревшее
|
||||
сохранение не подменяет текущее фото и его параметры. Панель рендерит только
|
||||
фото, совпадающее с текущим маршрутом.
|
||||
|
||||
**Пользовательские действия и файлы.**
|
||||
- Сохранить: диалог имени, кривые всех четырёх каналов; при совпадении имени —
|
||||
подтверждение перезаписи.
|
||||
- Переименовать/удалить: действия в меню пользовательского пресета.
|
||||
- Экспорт: JSON `{ version: 1, presets: CurvePreset[] }` с кривыми всех каналов,
|
||||
только пользовательские пресеты (встроенные всегда доступны).
|
||||
- Импорт: файл-пикер, валидация, upsert по имени, сохранение через preferences;
|
||||
при некорректном файле профиль не меняется.
|
||||
|
||||
**Совместимость формата.**
|
||||
- Старый формат `{ name, channel, points }` читается из профиля и импорта:
|
||||
точки попадают в указанный канал, остальные становятся линейными.
|
||||
- При следующем сохранении список перезаписывается новым форматом со всеми
|
||||
четырьмя каналами.
|
||||
|
||||
**UI.**
|
||||
- Статичный текст «Линейный» заменяется кнопкой, открывающей popover-меню:
|
||||
встроенные, пользовательские, «Сохранить…», «Импортировать…», «Экспортировать…».
|
||||
- В SVG крайние точки — такие же перетаскиваемые кружки, как остальные; первый
|
||||
клик по свободному полю вставляет точку между крайними; двойной клик сбрасывает.
|
||||
- Сохраняется существующая клавиатурная правка точек.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
[Старые кривые начнут выглядеть иначе (плавно вместо линейно)] → это осознанное
|
||||
изменение рендера; точки не мигрируются, плоские участки вне крайних сохраняются.
|
||||
|
||||
[Расхождение PCHIP в Go и TS] → одна формула, тесты с захардкоженными значениями
|
||||
LUT в обоих языках; WASM/WebGL используют готовый LUT.
|
||||
|
||||
[Гонка при мерже preferences] → мерж на сервере по разделам: разные разделы не
|
||||
затирают друг друга; одновременная запись одного раздела — last-write-wins,
|
||||
приемлемо для настроек одного пользователя.
|
||||
|
||||
[Рост списка пресетов / большой импорт] → лимиты количества, размера имени и
|
||||
точек; handler уже ограничивает тело 1 МБ.
|
||||
|
||||
[Старые пресеты хранятся в формате одного канала] → клиент конвертирует их при
|
||||
чтении/импорте, а первое сохранение пресетов перезаписывает профиль новым
|
||||
форматом.
|
||||
|
||||
[Пересечение точек при перетаскивании] → ограничение x соседями и нормализация
|
||||
на коммите.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Схема не меняется: `curve_presets` — ключ в JSON-блобе `users.preferences`.
|
||||
Старый формат пресетов конвертируется на клиенте при чтении. Откат: удалить
|
||||
ключ; старые кривые остаются в `light.curve` без изменений данных.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет. Точные точки «Среднего/Сильного контраста» — настраиваемый дефолт, не влияет
|
||||
на контракт.
|
||||
Reference in New Issue
Block a user